<?xml version="1.0"
            encoding="UTF-8" standalone="no"?><addons><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Adaptive_Next" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Direction" nameLabel="Adaptive_Next_property_next_direction" type="{Next,Previous}"/>
		<property name="Is disabled" nameLabel="Adaptive_Next_property_next_is_disabled" type="boolean"/>
		<property isDefault="true" name="Image" nameLabel="Adaptive_Next_property_next_image" type="image"/>
	</model>
<css>.adaptive-next-button-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.adaptive-next-button-element {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    cursor: pointer;
}

.adaptive-next-button-element-disabled {
    opacity: .35;
}

.adaptive-next-button-image {
    background-repeat: no-repeat;
    background-size: contain;
}

</css><view>&lt;div class="adaptive-next-wrapper"&gt;
    &lt;img class="adaptive-next-button-element adaptive-next-button-image"&gt;

    &lt;/img&gt;
&lt;/div&gt;
</view><preview>&lt;div class="adaptive-next-wrapper"&gt;
    &lt;div class="adaptive-next-button-element adaptive-next-button-image"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonAdaptive_Next_create() {
    var presenter = function() {};

    presenter.isAdaptivePreviewMode = false;

    presenter.CONSTANTS = {
        NEXT_IMAGE: 'baseline-navigate_next-24px.svg',
        PREV_IMAGE: 'baseline-navigate_before-24px.svg',
        DISABLED_CLASS: 'adaptive-next-button-element-disabled',
        ELEMENT_CLASS: 'adaptive-next-button-element'
    };

    presenter.BUTTON_TYPE = {
        NEXT: 'Next',
        PREV: 'Previous'
    };

    presenter.state = {
        isVisible: true,
        isDisabled: false, // based on commands
        isErrorMode: false
    };

    presenter.internalState = {
        isDisabled: false // based on page location in steps
    }

    presenter.isDisabled = function () {
        return presenter.state.isDisabled || presenter.internalState.isDisabled;
    }

    presenter.executeUserEventCode = function() {
        if (presenter.playerController == null) return;
        if (presenter.configuration.onClickEvent.isEmpty) return;

        presenter.playerController.getCommands().executeEventCode(presenter.configuration.onClickEvent.value);
    };

    presenter.clickHandler = function (event) {
        if (event !== undefined) {
            event.stopPropagation();
        }

        if (presenter.isDisabled()) return;

        presenter.triggerButtonClickedEvent();
    };

    function handleMouseActions() {
        var $element = presenter.$view.find('.' + presenter.CONSTANTS.ELEMENT_CLASS);
        $element.click(presenter.clickHandler);
    }

    function setElementsDimensions(model, wrapper, element) {
        var viewDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var viewDistances = DOMOperationsUtils.calculateOuterDistances(viewDimensions);
        presenter.$view.css({
            width:(model.Width - viewDistances.horizontal) + 'px',
            height:(model.Height - viewDistances.vertical) + 'px'
        });

        DOMOperationsUtils.setReducedSize(presenter.$view, wrapper);
        DOMOperationsUtils.setReducedSize(wrapper, element);
    }

    function addImageBackground(element) {
        var resource = presenter.getResourceName();
        var source = getImageUrlFromResources(resource);

        if (presenter.configuration.Image) {
            source = presenter.configuration.Image;
        }

        element.attr('src', source);
    }

    presenter.isNextButton = function() {
        return presenter.configuration.Direction === presenter.BUTTON_TYPE.NEXT
    }

    presenter.getResourceName = function () {
        if (presenter.isNextButton()) {
            return presenter.CONSTANTS.NEXT_IMAGE;
        } else {
            return presenter.CONSTANTS.PREV_IMAGE;
        }
    };

    presenter.createElement = function($element) {
        if (presenter.configuration.isTabindexEnabled) {$element.attr('tabindex', '0');}

        addImageBackground($element);
    };

    presenter.initView = function () {
        var $wrapper = $(presenter.$view.find('.adaptive-next-wrapper')[0]);
        var $element = presenter.$view.find('.' + presenter.CONSTANTS.ELEMENT_CLASS);

        presenter.createElement($element);

        setElementsDimensions(presenter.configuration, $wrapper, $element);
        presenter.toggleDisable(presenter.configuration.isDisabled);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        if (presenter.adaptiveLearningService) {
            presenter.addStepBasedBehaviour();
        }
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.$view.get(0)) {
            return;
        }

        presenter.$view.find('.' + presenter.CONSTANTS.ELEMENT_CLASS).off("click", presenter.clickHandler);
    };

    function presenterLogic(view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.view = view;
        presenter.$view = $(view);

        var validatedModel = presenter.validateModel(model);

        if (!validatedModel.isValid) {
            return;
        }

        presenter.configuration = validatedModel.value;


        presenter.initView();

        if (!isPreview) {
            MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.$view.get(0));
            MutationObserverService.setObserver();
            handleMouseActions();
        }
    }

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.adaptiveLearningService = presenter.playerController.getAdaptiveLearningService();

        var eventBus = presenter.playerController.getEventBus();

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };


    presenter.validateString = function (imageSrc) {
        var isEmpty = ModelValidationUtils.isStringEmpty(imageSrc);

        return {
            isEmpty: isEmpty,
            value: isEmpty ? "" : imageSrc
        };
    };

    presenter.validateModel = function (model) {
        var modelValidator = new ModelValidator();

        var validatedModel = modelValidator.validate(model, [
            ModelValidators.utils.FieldRename("Is Visible", "isVisible", ModelValidators.Boolean('isVisible')),
            ModelValidators.Enum('Direction', {
                default: presenter.BUTTON_TYPE.NEXT,
                values: [presenter.BUTTON_TYPE.NEXT, presenter.BUTTON_TYPE.PREV]
            }),
            ModelValidators.utils.FieldRename("Is disabled", "isDisabled", ModelValidators.Boolean('isDisabled')),
            ModelValidators.Integer('Width'),
            ModelValidators.Integer('Height'),
            ModelValidators.DumbString('ID'),
            ModelValidators.String('Image', {'default': null})
        ]);

        return validatedModel;
    };

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enable,
            'disable': presenter.disable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.nextButtonTrigger = function() {
        // if there is next page in saved history, move to that page, otherwise check connections for current page
        if (presenter.adaptiveLearningService.isNextAdaptivePageAvailable()) {
            presenter.adaptiveLearningService.moveToNextPage();
            return;
        }

        // this allows to inject custom pages states into window object, which will be used instead of player state
        presenter.isAdaptivePreviewMode = window.adaptivePreviewMode ? true : false;

        var connections = presenter.adaptiveLearningService.getCurrentPageConnections();

        for (var i = 0; i &lt; connections.length; i++) {
            var isConditionMet = presenter.evaluateCondition(connections[i].conditions);

            if (isConditionMet) {
                presenter.adaptiveLearningService.addAndMoveToNextPage(connections[i].target);
                return;
            }
        }
    };

    presenter.prevButtonTrigger = function() {
        presenter.adaptiveLearningService.moveToPrevPage();
    };


    presenter.triggerButtonClickedEvent = function() {
        if (presenter.adaptiveLearningService == null) return;

        if (presenter.configuration.Direction === presenter.BUTTON_TYPE.PREV) {
            presenter.prevButtonTrigger();
        } else {
            presenter.nextButtonTrigger();
        }
    };

    presenter.evaluateCondition = function(condition) {
        if (condition === '') {
            return true;
        }
        try {
            return eval(condition);
        } catch (e) {
            return false;
        }
    };

    // needed for condition evaluation
    function expect(pageID) {
        if (presenter.isAdaptivePreviewMode) {
            return window.pagesScores[pageID];
        } else {
            var scoreService = presenter.playerController.getScore();

            return scoreService.getPageScoreById(pageID);
        }
    }


    presenter.setVisibility = function(isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        this.setVisibility(true);
        presenter.state.isVisible = true;
    };

    presenter.hide = function() {
        this.setVisibility(false);
        presenter.state.isVisible = false;
    };

    presenter.reset = function() {
        presenter.state.isErrorMode = false;
        presenter.state.isVisible = presenter.configuration.isVisible;
        if (presenter.configuration.isVisible) {
            this.show();
        } else {
            this.hide();
        }
        presenter.toggleDisable(this.configuration.isDisabledByDefault);
    };

    presenter.enable = function() {
        this.toggleDisable(false);
    };

    presenter.disable = function() {
        this.toggleDisable(true);
    };

    presenter.toggleDisable = function(disable) {
        presenter.addDisabledClasses(disable);
        presenter.state.isDisabled = disable;
    };

    presenter.addDisabledClasses = function(disable) {
        var element = presenter.$view.find('.' + presenter.CONSTANTS.ELEMENT_CLASS);
        if (disable) {
            element.addClass(presenter.CONSTANTS.DISABLED_CLASS);
        } else {
            element.removeClass(presenter.CONSTANTS.DISABLED_CLASS);
        }
    }

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.state.isVisible,
            isDisabled: presenter.state.isDisabled
        });
    };

    presenter.setState = function(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;
        var state = JSON.parse(stateString);

        presenter.state.isDisabled = state.isDisabled;
        presenter.state.isVisible = state.isVisible;

        if (presenter.state.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.toggleDisable(presenter.state.isDisabled);
        if (presenter.adaptiveLearningService) {
            presenter.addStepBasedBehaviour();
        }
    };

    presenter.setShowErrorsMode = function () {
        presenter.state.isErrorMode = true;
    };

    presenter.setWorkMode = function () {
        presenter.state.isErrorMode = false;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.state.isErrorMode = true;
        }

        if (eventName == "HideAnswers") {
            presenter.state.isErrorMode = false;
        }
    };

    presenter.keyboardController = function(keyCode, isShiftDown, event) {
        event.preventDefault();
        if (keyCode == window.KeyboardControllerKeys.ENTER) {
            presenter.clickHandler();
        }
    };

    presenter.addStepBasedBehaviour = function() {
        var shouldBeDisabled = (presenter.adaptiveLearningService.isFirstStep() &amp;&amp; !presenter.isNextButton()) ||
            (presenter.adaptiveLearningService.isLastStep() &amp;&amp; presenter.isNextButton());

        presenter.internalState.isDisabled = shouldBeDisabled;
        if (shouldBeDisabled) {
            presenter.addDisabledClasses(shouldBeDisabled);
        }
    }

    function getImageUrlFromResources (fileName) {
        if (!presenter.playerController) {
            return '';
        }
        return presenter.playerController.getStaticFilesPath() + 'addons/resources/' + fileName;
    }
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Advanced_Connector" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Scripts" nameLabel="Advanced_Connector_property_scripts" type="editablescript"/>
        <property name="Is disabled" nameLabel="Advanced_Connector_property_is_disabled" type="boolean"/>
	</model>
<css/><view/><preview/><presenter>function AddonAdvanced_Connector_create() {
    var presenter = function () {};
    var event;

    presenter.STANDARD_EVENTS = [
        'ValueChanged',
        'Definition',
        'ItemSelected',
        'ItemConsumed',
        'ItemReturned',
        'PageLoaded',
        'PageAllOk',
        'ShowAnswers',
        'HideAnswers',
        'Done',
        'AllAttempted',
        'NotAllAttempted',
        'LimitedCheck',
        'GradualShowAnswers',
        'GradualHideAnswers',
    ];

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (presenter.isDisabled) {
            return;
        }
        var i, length;
        const previousEvent = event;
        let filledEventData = presenter.fillEventData(eventData, eventName);

        try {
            var filteredEvents = presenter.filterEvents(presenter.events, filledEventData);
            for (i = 0, length = filteredEvents.length; i &lt; length; i++) {
                event = filledEventData;
                eval(filteredEvents[i].Code);
            }
        } catch (error) {
            Helpers.alertErrorMessage(error, "Advanced Connector - problem occurred while running scripts!");
        }

        event = previousEvent;
    };

    presenter.reset = function () {
        presenter.onEventReceived('Reset', {});
    };

    presenter.setShowErrorsMode = function () {
        presenter.onEventReceived('Check', {});
    };

    presenter.setWorkMode = function () {
        presenter.onEventReceived('Uncheck', {});
    };

    function isCustomEvent(eventName) {
        return $.inArray(eventName, presenter.STANDARD_EVENTS) == -1;
    }

    presenter.run = function(view, model) {
        var validatedScript = presenter.validateScript(model.Scripts), eventBus,
            customEventListeners = [];

        if (validatedScript.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedScript.errorCode);
            return;
        }

        presenter.isDisabled = ModelValidationUtils.validateBoolean(model["Is disabled"]);
        if (presenter.isDisabled) {
            return;
        }

        eventBus = presenter.playerController.getEventBus();
        presenter.events = validatedScript.events;

        $.each(presenter.STANDARD_EVENTS, function(_, name) {
            eventBus.addEventListener(name, presenter);
        });

        $.each(presenter.events, function() {
            if (isCustomEvent(this.Name) &amp;&amp; (customEventListeners.indexOf(this.Name) == -1) ){
                eventBus.addEventListener(this.Name.trim(), presenter);
                customEventListeners.push(this.Name);
            }
        });

        $(view).css('visibility', 'hidden');
    };

    presenter.containsFileServeLink = function(script) {
        script = script.replace(/\s/g,'');
        const regex = new RegExp("(\/)*file\/serve\/[0-9]+");

        return regex.test(script);
    };

    presenter.createPreview = function(view, model) {
        const validatedScript = presenter.validateScript(model.Scripts);
        if (validatedScript.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedScript.errorCode);
        }
        if (presenter.containsFileServeLink(model.Scripts)) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, 'SV_09');
        }
    };

    presenter.ERROR_CODES = {
        'SV_01': "Script source must be provided and cannot be empty!",
        'SV_02': "Missing EVENTSTART keyword or new line after it!",
        'SV_03': "Missing EVENTEND keyword or new line after it!",
        'SV_04': "Missing SCRIPTSTART keyword or new line after it!",
        'SV_05': "Missing SCRIPTEND keyword or new line after it!",
        'SV_06': "Repeated event field value declaration!",
        'SV_07': "Repeated keyword!",
        'SV_08': "Invalid identification. Should be Source,Item, Value or Score!",
        'SV_09': "Please ensure that resources are linked correctly. For more information, refer to this addon's documentation."
    };

    function returnErrorResult(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.fillBlankFields = function(eventDeclaration) {
        for (var prop in eventDeclaration) {
            if (eventDeclaration.hasOwnProperty(prop) &amp;&amp; prop !== 'Name') {
                if (eventDeclaration[prop] == undefined) {
                    eventDeclaration[prop] = '.*';
                }
            }
        }

        if (eventDeclaration.Name == undefined) {
            eventDeclaration.Name = 'ValueChanged';
        }
    };

    presenter.findKeywordIndex = function(script, keyword, currentLine) {
        for (var i = currentLine + 1; i &lt; script.length; i++) {
            if (script[i].trim() === keyword) return i;
        }

        return -1;
    };

    presenter.validateEvent = function(script) {
        var indexes = [], i, length;
        var eventDeclaration = {
            Source: undefined,
            Item: undefined,
            Value: undefined,
            Score: undefined,
            Name: undefined,
            Word: undefined,
            Type: undefined,
            Code: ''
        };

        for(i = 0, length = script.length; i &lt; length; i++) {
            indexes[i] = false;
        }

        var scriptStartIndex = presenter.findKeywordIndex(script, "SCRIPTSTART", -1);
        var scriptEndIndex = presenter.findKeywordIndex(script, "SCRIPTEND", 1);

        var isScriptInvalid = scriptStartIndex === -1 &amp;&amp; scriptEndIndex !== -1;
        isScriptInvalid = isScriptInvalid || scriptStartIndex !== -1 &amp;&amp; scriptEndIndex === -1;
        isScriptInvalid = isScriptInvalid || scriptStartIndex &gt; scriptEndIndex;

        if (isScriptInvalid) return returnErrorResult('SV_04');

        indexes[scriptStartIndex] = true;
        indexes[scriptEndIndex] = true;

        for (i = scriptStartIndex + 1; i &lt; scriptEndIndex; i++) {
            indexes[i] = true;
            eventDeclaration.Code += script[i];
            if (i !== scriptEndIndex - 1) {
                eventDeclaration.Code += '\n';
            }
        }

        for(i = 0, length = indexes.length; i &lt; length; i++) {
            if (indexes[i]) continue;

            var line = script[i].split(':');
            if (line.length !== 2) return returnErrorResult('SV_08');

            var keywords = ['Source', 'Item', 'Value', 'Score', 'Name', 'Type', 'Word'];

            var trimmedKeyword = line[0].trim();
            if (keywords.indexOf(trimmedKeyword) === -1) return returnErrorResult('SV_08');

            if (eventDeclaration[trimmedKeyword]) {
                return returnErrorResult('SV_06');
            } else {
                eventDeclaration[trimmedKeyword] = line[1];
            }
        }

        presenter.fillBlankFields(eventDeclaration);
        eventDeclaration.Code = parseLinks(eventDeclaration.Code);

        return { isError: false, eventDeclaration: eventDeclaration };
    };

    function parseLinks(code) {
        let baseURL = getBaseURL();
        if (!baseURL) {
            return code;
        }

        const linkRegexp = /(['"`])(\.\.\/resources\/[0-9]+\.[a-zA-Z0-9]+)/g;
        const matches = code.matchAll(linkRegexp);
        const matchesArray = [...matches];
        if (matchesArray.length === 0) {
            return code;
        }

        let parsedCode = "";
        let previousEndIndex = 0;
        for (let i = 0; i &lt;= matchesArray.length - 1; i++) {
            const match = matchesArray[i];
            const fullMatch = match[0];
            const prefix = match[1]; // from (['"`])
            const relativePath = match[2]; // from (\.\.\/resources\/[0-9]+\.[a-zA-Z0-9]+)
            const startIndex = match.index;
            const endIndex = match.index + fullMatch.length;

            const newURL = new URL(relativePath, baseURL);
            const signedNewURL = presenter.playerController.getRequestsConfig().signURL(newURL.href);

            parsedCode += code.substring(previousEndIndex, startIndex);
            parsedCode += prefix + signedNewURL;
            previousEndIndex = endIndex;
        }
        parsedCode += code.substring(previousEndIndex, code.length);

        return parsedCode;
    }

    function getBaseURL() {
        if (!presenter.playerController) {
            return null;
        }
        const contextMetadata = presenter.playerController.getContextMetadata();
        const contentBaseURL = !!contextMetadata ? contextMetadata['contentBaseURL'] : null;
        if (!!contentBaseURL) {
            return contentBaseURL;
        }
        const pageIndex = presenter.playerController.getCurrentPageIndex();
        return presenter.playerController.getPresentation().getPage(pageIndex).getBaseURL();
    }

    function extractLines(script, start, end) {
        var array = [];

        for (var i = start; i &lt; end; i++) array.push(script[i]);

        return array;
    }

    function cleanScriptFromEmptyLines(script) {
        var array = [];

        for (var i = 0; i &lt; script.length; i++) {
            if (script[i]) array.push(script[i]);
        }

        return array;
    }

    presenter.validateScript = function (script) {
        if (!script) return returnErrorResult('SV_01');

        var scriptsArray = [];
        var scriptLines = cleanScriptFromEmptyLines(script.split('\n'));
        var lineIndex = 0, length = scriptLines.length;

        while (lineIndex &lt; length) {
            if (scriptLines[lineIndex] !== "EVENTSTART") return returnErrorResult('SV_02');

            var endEventIndex = presenter.findKeywordIndex(scriptLines, "EVENTEND", lineIndex);
            if (endEventIndex === -1) return returnErrorResult('SV_03');

            lineIndex++;
            var startEventIndex = presenter.findKeywordIndex(scriptLines, "EVENTSTART", lineIndex);

            if (startEventIndex !== -1 &amp;&amp; startEventIndex &lt; endEventIndex) return returnErrorResult('SV_07');

            var eventCode = extractLines(scriptLines, lineIndex, endEventIndex);
            var validatedEvent = presenter.validateEvent(eventCode);
            if (validatedEvent.isError) {
                return returnErrorResult(validatedEvent.errorCode);
            } else {
                scriptsArray.push(validatedEvent.eventDeclaration);
            }

            lineIndex = endEventIndex + 1;
        }

        return { isError: false, events: scriptsArray };
    };

    presenter.matchFieldToRule = function (field, rule, doExactMatch) {
        if (rule.length &gt; 0)
            var rule = doExactMatch ? '^' + $.trim(rule) + '$' : $.trim(rule);
        return new RegExp(rule).test(field);
    };

    presenter.filterEvents = function (events, event) {
        var filteredArray = [], isMatch;

        try {
            for (var i = 0, length = events.length; i &lt; length; i++) {
                isMatch = presenter.matchFieldToRule(event.name, events[i].Name, true);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.source, events[i].Source);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.item, events[i].Item);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.value, events[i].Value);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.score, events[i].Score);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.word, events[i].Word);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.type, events[i].Type);

                if (isMatch) {
                    filteredArray.push(events[i]);
                }
            }
        } catch (error) {
            Helpers.alertErrorMessage(error, "Advanced Connector - problem occurred while processing conditions!");
        }

        return filteredArray;
    };

    presenter.fillEventData = function (eventData, eventName) {
        var filledEventData = $.extend(true, {}, eventData);
        filledEventData.name = eventName;

        if (!filledEventData.word) filledEventData.word = '';
        if (!filledEventData.type) filledEventData.type = '';
        if (!filledEventData.item) filledEventData.item = '';
        if (!filledEventData.value) filledEventData.value = '';
        if (!filledEventData.source) filledEventData.source = '';
        if (!filledEventData.score) filledEventData.score = '';

        return filledEventData;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Animated_Lesson_Progress" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Ranges" nameLabel="Animated_Lesson_Progress_property_ranges" type="list">
            <property name="Score" nameLabel="Animated_Lesson_Progress_property_score" type="string"/>
		    <property name="Image" nameLabel="Animated_Lesson_Progress_property_image" type="image"/>
		</property>
	</model>
<css>.animated-lesson-progress-wrapper{
    width: 100%;
    height: 100%;
}

.animated-lesson-progress-rate {
    width: 100%;
    height: 100%;
    background-size: 100% 100%;
    background-repeat: no-repeat;
}
</css><view>&lt;div class="animated-lesson-progress-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="animated-lesson-progress-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonAnimated_Lesson_Progress_create() {
    var presenter = function () { };

    var range_img = [],
        range_max_score = [];

    var playerController;
    var eventBus;

    presenter.ERROR_CODES = {
        'E_01': "All ranges must be in ascending order",
        'E_02': "Last range must equal 100",
        'E_03': "All ranges must be positive",
        'E_04': "First range must equal 0",
        'E_05': "All scores must be filled"
    };

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.sanitizeModel = function (model)  {

        for (var ranges_prop=0; ranges_prop &lt; model.Ranges.length; ranges_prop++){
            range_img[ranges_prop] = model.Ranges[ranges_prop].Image;
            range_max_score[ranges_prop] = parseFloat(model.Ranges[ranges_prop].Score);
        }

        for (var i=0; i&lt; model.Ranges.length; i++){
            if(!model.Ranges[i].Score){
                return returnErrorObject('E_05');
            }
            if(range_max_score[i]&gt; range_max_score[i+1]){
                return returnErrorObject('E_01');
            }
            if(range_max_score[i] &lt; 0){
                return returnErrorObject('E_03');
            }
        }

        if(range_max_score[model.Ranges.length-1] != 100){
            return returnErrorObject('E_02');
        }

        if(range_max_score[0] != 0){
            return returnErrorObject('E_04');
        }

        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);

        return {
            isError: false,
            Ranges: {
                Image: range_img,
                deselected: range_max_score
            },
            length: model.Ranges.length,
            isVisible: isVisible

        }
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    presenter.cleanView = function () {
        presenter.$view.find('.animated-lesson-progress-rate').each(function () {
            $(this).css('display', 'none');
            $(this).attr('data-name', 'invisible');
        });
    };

    presenter.setViewImage = function (rate) {
        presenter.$view.find('.rate-' + (rate+1)).css('display', 'block');
        presenter.$view.find('.rate-' + (rate+1)).attr('data-name', 'visible');
    };

    presenter.countPercentageScore = function () {
        var model = playerController.getPresentation();
        var scoreService = playerController.getScore();
        var count = 0,
            percentageScore = 0;

        for(var i = 0; i &lt; model.getPageCount(); i++){
            var page = model.getPage(i);

            if(page.isReportable()){
                count += 1;

                var pageScore = scoreService.getPageScoreById(page.getId()),
                    score = pageScore.score,
                    maxScore = pageScore.maxScore;

                if (maxScore &gt; 0) {
                    percentageScore += ((score/maxScore) * 100);
                } else if (page.isVisited()) {
                    percentageScore += 100; // the page was visited but there is no activities, so we give you a bonus +100% :-)
                }

            }
        }

        if (count &gt; 0) {
            percentageScore = percentageScore / count;
        }

        for (var j = 0; j &lt; range_max_score.length; j++){
            if(percentageScore == 0){
                presenter.cleanView();
                presenter.setViewImage(0);
                return;
            }

            if(percentageScore &lt;= range_max_score[j+1] &amp;&amp; percentageScore &gt; range_max_score[j]){
                presenter.cleanView();
                presenter.setViewImage(j+1);
            }
        }

    };

    presenter.appendImages = function (length) {
        for (var j=0; j&lt;length; j++){
            presenter.$view.find('.animated-lesson-progress-wrapper').append('&lt;div class="animated-lesson-progress-rate rate-'+ (j+1) +'"&gt;&lt;/div&gt;');
            if(range_img[j] != "") {
                presenter.$view.find('.rate-' + (j + 1)).css('background-image', 'url(' + range_img[j] + ')');
            }
            presenter.$view.find('.rate-'+(j+1)).css('display', 'none');
        }
    };

    presenter.eventListener = function () {
        eventBus = playerController.getEventBus();
        presenter.countPercentageScore();
        eventBus.addEventListener('ValueChanged', this);
        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.presenterLogic = function (view, model, isPreview) {
    	presenter.$view = $(view);
    	presenter.configuration = presenter.sanitizeModel(model);

        if(presenter.configuration.isError){
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.appendImages(presenter.configuration.length);

        if(!isPreview) {
            presenter.eventListener();
        }else{
            presenter.setViewImage(0);
        }

    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ValueChanged" &amp;&amp; !presenter.isShowAnswersActive) {
            presenter.countPercentageScore();
        }

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.getImageId = function () {
        return presenter.$view.find('[data-name="visible"]').attr('id');
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };
    
    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };


    presenter.getState = function () {
        if (presenter.configuration.isError) {
            return "";
        }

    	return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
       if (!state) return;

    	var parsedState = JSON.parse(state);

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);
    };
    
    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.reset = function () {
        presenter.countPercentageScore();
    };

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
    };

    presenter.hideAnswers = function () {
        presenter.isShowAnswersActive = false;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Animated_Page_Progress" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Ranges" nameLabel="Animated_Page_Progress_property_ranges" type="list">
            <property name="Score" nameLabel="Animated_Page_Progress_property_score" type="string"/>
		    <property name="Image" nameLabel="Animated_Page_Progress_property_image" type="image"/>
		</property>
        <property name="Initial image" nameLabel="Animated_Page_Progress_property_initial_image" type="image"/>
        <property displayName="Work in Check Mode" name="Work in Check Mode" nameLabel="Animated_Page_Progress_property_work_in_check_mode" type="boolean"/>
	</model>
<css>.animated-page-progress-wrapper{
    width: 100%;
    height: 100%;
}

.animated-page-progress-rate {
    width: 100%;
    height: 100%;
    background-size: 100% 100%;
    background-repeat: no-repeat;
}
</css><view>&lt;div class="animated-page-progress-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="animated-page-progress-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonAnimated_Page_Progress_create() {
    var presenter = function () {};

    var playerController;
    var eventBus;

    presenter.displayedImage = null;
    presenter.ERROR_CODES = {
        'E_01': "All ranges must be in ascending order",
        'E_02': "Last range must equal 100",
        'E_03': "All ranges must be positive",
        'E_04': "First range must equal 0",
        'E_05': "All scores must be filled"
    };

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.sanitizeModel = function (model) {
        var rangeImage = [], rangeMaxScore = [], i;

        for (i = 0; i &lt; model.Ranges.length; i++) {
            rangeImage[i] = model.Ranges[i].Image;
            rangeMaxScore[i] = parseFloat(model.Ranges[i].Score);
        }

        for (i = 0; i &lt; model.Ranges.length; i++) {
            if (!model.Ranges[i].Score) {
                return returnErrorObject('E_05');
            }
            if (rangeMaxScore[i] &gt; rangeMaxScore[i + 1]) {
                return returnErrorObject('E_01');
            }
            if (rangeMaxScore[i] &lt; 0) {
                return returnErrorObject('E_03');
            }
        }

        if (rangeMaxScore[model.Ranges.length - 1] != 100) {
            return returnErrorObject('E_02');
        }

        if (rangeMaxScore[0] != 0) {
            return returnErrorObject('E_04');
        }

        return {
            isError: false,
            ranges: {
                images: rangeImage,
                maxScores: rangeMaxScore
            },
            length: model.Ranges.length,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            initialImage: model['Initial image'],
            workInCheckMode: ModelValidationUtils.validateBoolean(model['Work in Check Mode'])
        }
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
        var currentPageIndex = playerController.getCurrentPageIndex();
        presenter.pageID = playerController.getPresentation().getPage(currentPageIndex).getId();
        presenter.scoreService = playerController.getScore();
    };

    presenter.cleanView = function () {
        presenter.$view.find('.animated-page-progress-rate').each(function () {
            $(this).css('display', 'none');
            $(this).attr('data-name', 'invisible');
        });
    };

    presenter.setViewImage = function (rate) {
        var $rate;

        if (rate == "initial") {
            $rate = presenter.$view.find('.rate-initial');
        } else {
            $rate = presenter.$view.find('.rate-' + (rate + 1));
        }

        $rate.css('display', 'block');
        $rate.attr('data-name', 'visible');

        presenter.displayedImage = rate;
    };

    presenter.getRange = function (pageScore) {
        var score = pageScore.score,
            maxScore = pageScore.maxScore,
            percentageScore = (score / maxScore) * 100,
            maxScores = presenter.configuration.ranges.maxScores;

        if (isNaN(percentageScore)) {
            percentageScore = 0;
        }

        for (var i = 0; i &lt; maxScores.length; i++) {
            if (percentageScore == 0) {
                return 0;
            }
            if (percentageScore == 100 &amp;&amp; maxScores[i] == 100) {
                return i;
            }
            if (percentageScore &gt;= maxScores[i] &amp;&amp; percentageScore &lt; maxScores[i + 1]) {
                return i;
            }
        }
    };

    presenter.changeRange = function () {
        var pageScore = presenter.scoreService.getPageScoreById(presenter.pageID),
            range = presenter.getRange(pageScore);

        presenter.cleanView();
        presenter.setViewImage(range);
    };

    presenter.appendImages = function (length) {
        var $wrapper = presenter.$view.find('.animated-page-progress-wrapper'),
            images = presenter.configuration.ranges.images;

        for (var i = 0; i &lt; length; i++) {
            var $rate = $(document.createElement('div'));

            $rate.addClass('animated-page-progress-rate rate-' + (i + 1)).css('display', 'none');

            if (images[i] != "") {
                $rate.css('background-image', 'url(' + images[i] + ')');
            }

            $wrapper.append($rate);
        }

        if (presenter.configuration.initialImage) {
            var $initialRate = $(document.createElement('div'));

            $initialRate.addClass('animated-page-progress-rate rate-initial');
            $initialRate.css({
                'display': 'none',
                'background-image': 'url(' + presenter.configuration.initialImage + ')'
            });

            $wrapper.append($initialRate);
        }
    };

    presenter.eventListener = function () {
        eventBus = playerController.getEventBus();
        eventBus.addEventListener('ValueChanged', this, true);
        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
        eventBus.addEventListener('PageLoaded', this);
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.configuration = presenter.sanitizeModel(model);

        if (presenter.configuration.isError){
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.appendImages(presenter.configuration.ranges.images.length);

        if (!isPreview) {
            presenter.eventListener();
        }

        presenter.setViewImage(presenter.configuration.initialImage ? "initial" : 0);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "ValueChanged":
                if (!presenter.isShowAnswersActive &amp;&amp; !presenter.configuration.workInCheckMode ||
                    !presenter.isShowAnswersActive &amp;&amp; presenter.configuration.workInCheckMode &amp;&amp; eventData.value === "resetClicked") {
                    presenter.changeRange();
                }
                break;

            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "HideAnswers":
                presenter.hideAnswers();
                break;

            case "PageLoaded":
                presenter.changeRange();
                break;
        }
    };

    presenter.getImageId = function () {
        return presenter.$view.find('[data-name="visible"]').attr('id');
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };
    
    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.isCommonsPage = function() {
        return presenter.pageID != presenter.$view.parent().attr("id");
    };

    presenter.getCurrentPageIndex = function() {
        return playerController.getCurrentPageIndex();
    };

    presenter.getState = function () {
        if (presenter.configuration.isError) {
            return "";
        }

        var currentPageIndex = presenter.getCurrentPageIndex();

        if(presenter.isCommonsPage()) {
            if (typeof presenter.states == "undefined") {
                presenter.states = {};
                presenter.pageState = {};
            }

            presenter.pageState = {
                displayedImage: presenter.displayedImage,
                isVisible: presenter.configuration.isVisible
            };

            presenter.states[currentPageIndex] = presenter.pageState;

            return JSON.stringify(presenter.states);
        } else {
            return JSON.stringify({
                pageIndex: currentPageIndex,
                displayedImage: presenter.displayedImage,
                isVisible: presenter.configuration.isVisible
            });
        }
    };

    presenter.setState = function (state) {
        if (!state) return;

        var currentPageIndex = presenter.getCurrentPageIndex(),
            displayedImage,
            parsedState;

        presenter.states = JSON.parse(state);

        if(presenter.isCommonsPage()) {
            parsedState = presenter.states[currentPageIndex];
        } else {
            parsedState = presenter.states;
        }

        if (typeof parsedState == "undefined") {
            parsedState = '';
            displayedImage = presenter.configuration.initialImage ? "initial" : 0;
        } else {
            displayedImage = parsedState.displayedImage;
        }

        presenter.cleanView();
        presenter.setViewImage(displayedImage);
        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.setShowErrorsMode = function(){
        if(presenter.configuration.workInCheckMode){
            presenter.changeRange();
        }
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.reset = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.cleanView();
        var pageOpenActivitiesScore = presenter.scoreService.getPageScoreWithOnlyActiveOpenActivitiesById(presenter.pageID);
        if (pageOpenActivitiesScore &gt; 0) {
            var pageScore = presenter.scoreService.getPageScoreById(presenter.pageID);
            pageScore.score = pageOpenActivitiesScore;
            var range = presenter.getRange(pageScore);
            presenter.setViewImage(range);
        } else {
            presenter.setViewImage(presenter.configuration.initialImage ? "initial" : 0);
        }
    };

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
    };

    presenter.hideAnswers = function () {
        presenter.isShowAnswersActive = false;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Animation" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Preview image" nameLabel="Animation_property_preview_image" type="image"/>
		<property name="Animation" nameLabel="Animation_property_animation" type="image"/>
		<property name="Frames count" nameLabel="Animation_property_frames_count" type="string"/>
		<property name="Frame duration" nameLabel="Animation_property_frame_duration" type="string"/>
		<property name="Frame size" nameLabel="Animation_property_frame_size" type="{Original, Keep aspect ratio, Stretch}"/>
		<property name="Loop" nameLabel="Animation_property_loop" type="boolean"/>
		<property name="Labels" nameLabel="Animation_property_labels" type="list">
			<property isLocalized="true" name="Text" nameLabel="Animation_property_text" type="string"/>
			<property name="Top" nameLabel="Animation_property_top" type="string"/>
			<property name="Left" nameLabel="Animation_property_left" type="string"/>
			<property name="Frames" nameLabel="Animation_property_frames" type="string"/>
		</property>
		<property name="Show watermark" nameLabel="Animation_property_show_watermark" type="boolean"/>
		<property name="Watermark color" nameLabel="Animation_property_watermark_color" type="string"/>
		<property name="Watermark opacity" nameLabel="Animation_property_watermark_opacity" type="string"/>
		<property name="Watermark size" nameLabel="Animation_property_watermark_size" type="string"/>
		<property name="Don't reset on end" nameLabel="Animation_property_do_not_reset_on_end" type="boolean"/>
		<property name="Is click disabled" nameLabel="Animation_property_is_click_disabled" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Animation_property_speech_texts" type="staticlist">
            <property name="Stop" nameLabel="Animation_property_stop_speech_text" type="staticrow">
                <property name="Stop" nameLabel="Animation_property_stop_speech_text" type="string"/>
            </property>
		</property>
		<property displayName="Alternative Text" name="Alternative Text" nameLabel="Animation_property_alt_text" type="text"/>
		<property displayName="Preview Alternative Text" name="Preview Alternative Text" nameLabel="Animation_property_alt_text_prev" type="text"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Animation_property_lang_attribute" type="string"/>
		<property name="Base width" nameLabel="Animation_property_base_width" type="string"/>
		<property name="Base height" nameLabel="Animation_property_base_height" type="string"/>
	</model>
<css>.animation-image-animation,
.animation-image-preview,
.animation-loading-image,
.animation-label {
    border: 0;
    margin: 0;
    padding: 0;
}

.animation-image-animation,
.animation-image-preview {
    border: none;
    float: left;
    overflow: hidden;
    background: no-repeat 0 0;
}

.animation-image-animation {
    display: none;
}

.animation-image-preview {
    position: relative;
}

.animation-loading-image {
    display: none;
    position: absolute;
    border: 0 solid black;
    width: 50px;
    height: 50px;
}

.animation-label {
    position: absolute;
}

.animation-hidden-image {
    visibility: hidden;
}

.animation-watermark {
    border: 0;
    margin: 0;
    padding: 0;
    display: none;
    position: absolute;
}
</css><view>&lt;div class='animation-image-preview'&gt;
&lt;/div&gt;
&lt;div class='animation-image-animation'&gt;
&lt;/div&gt;
&lt;div class="animation-watermark"&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="animation-loading-image"/&gt;
</view><preview>&lt;div class='animation-image-preview'&gt;
&lt;/div&gt;
&lt;div class='animation-image-animation'&gt;
&lt;/div&gt;
&lt;div class="animation-watermark"&gt;
&lt;/div&gt;
&lt;img src="/media/images/loading.gif" alt="Loading..." class="animation-loading-image"/&gt;
</preview><presenter>function AddonAnimation_create (){
    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);

    var presenter = function () {};
    presenter.DOMElements = {};
    presenter.configuration = {};
    
    var isSpeaking = false; //tts is currently attempting to play the alternative text (but not the preview alt text)

    presenter.ERROR_CODES = {
        'AI_01': "Animation image wasn't set or was set incorrectly!",
        'PI_01': "Preview image wasn't set or was set incorrectly!",
        'FC_01': "Frames count must be positive integer number!",
        'FD_01': "Frame duration time must be positive integer number!",
        'L_01': "Text value cannot be empty!",
        'L_02': "Top position value is invalid!",
        'L_03': "Left position value is invalid!",
        'WM_01': "Watermark color must be provided in #RRGGBB format!",
        'WM_02': "Watermark opacity must be a value from 0.0 to 1.0!",
        'WM_03': "Watermark size must be a positive integer number!",
        'FL01': "Frame list - undefined or empty!",
        'FL02': "Frame list - syntax incorrect (probably wrong separator)!",
        'FL03': "Frame list - frame number invalid!",
        'FL04': "Frame list - frame number missing inside list!",
        'FL05': "Frame list - frame numbers range incorrect!",
        'ID_01': "Base width and height must be either positive integers or empty"
    };

    presenter.ANIMATION_STATE = {
        PAUSED: 0,
        PLAYING: 1,
        STOPPED: 2,
        ENDED: 3
    };

    presenter.FRAME_SIZE = {
        ORIGINAL: 0,
        SCALED: 1,
        STRETCHED: 2
    };

    presenter.IMAGE_TYPE = {
        PREVIEW: 0,
        ANIMATION: 1
    };

    presenter.eventBus = null;

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.addFramesToLabels(model);
        upgradedModel = presenter.upgradeTextToSpeech(upgradedModel);
        return presenter.upgradeBaseDimensions(upgradedModel);
    };

    presenter.addFramesToLabels = function (model) {
        var upgradedModel = {}, i;
        $.extend(true, upgradedModel, model); // Deep copy of model object

        for (i = 0; i &lt; model["Labels"].length; i++) {
            if (upgradedModel["Labels"][i]["Frames"] == undefined) {
                upgradedModel["Labels"][i]["Frames"] = "";
            }
        }

        return upgradedModel;
    };

    presenter.upgradeTextToSpeech = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel['Alternative Text']) {
            upgradedModel['Alternative Text'] = ''
        }

        if (!upgradedModel['Preview Alternative Text']) {
            upgradedModel['Preview Alternative Text'] = ''
        }

        if (!upgradedModel['speechTexts']) {
            upgradedModel['speechTexts'] = {
                Stop: {Stop: "stopped"}
            };
        }

        if (!upgradedModel['langAttribute']) {
            upgradedModel['langAttribute'] = '';
        }

        return upgradedModel;
    };

    presenter.upgradeBaseDimensions = function (model) {
        var upgradedModel = {}, i;
        $.extend(true, upgradedModel, model); // Deep copy of model object

            if (upgradedModel["Base width"] == undefined) {
                upgradedModel["Base width"] = "";
            }
            if (upgradedModel["Base height"] == undefined) {
                upgradedModel["Base height"] = "";
            }

        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.getspeechTexts = function(speechTexts) {
        var speechTexts = {
            stop:    getSpeechTextProperty(speechTexts['Stop']['Stop'], "Stopped")
        };

        return speechTexts;
    };

    function setDOMElementsHrefsAndSelectors(view) {
        presenter.DOMElements.viewContainer = $(view);
        presenter.DOMElements.animation = $(view).find('.animation-image-animation:first')[0];
        presenter.DOMElements.preview = $(view).find('.animation-image-preview:first')[0];
        presenter.DOMElements.loading = $(presenter.DOMElements.viewContainer.find('.animation-loading-image:first')[0]);
        presenter.DOMElements.watermark = $(presenter.DOMElements.viewContainer.find('.animation-watermark:first')[0]);
    }

    // Calculate scale for image containing element depending on frame aspect ratio
    presenter.calculateContainerDimensions = function(imageWidth, imageHeight, containerWidth, containerHeight) {
        var imageRatio = imageWidth / imageHeight;
        var containerRatio = containerWidth / containerHeight;

        var horizontal = imageRatio &gt;= containerRatio ? containerWidth : containerHeight * imageRatio;
        var vertical = imageRatio &gt;= containerRatio ? containerWidth / imageRatio : containerHeight;

        return {
            horizontal: horizontal,
            vertical: vertical
        };
    };

    function setElementsDimensions(wrapper) {
        var previewReducedSize = DOMOperationsUtils.calculateReducedSize(wrapper, presenter.DOMElements.preview);
        $(presenter.DOMElements.preview).css({
            width: previewReducedSize.width,
            height: previewReducedSize.height
        });

        var animationReducedSize = DOMOperationsUtils.calculateReducedSize(wrapper, presenter.DOMElements.animation);
        $(presenter.DOMElements.animation).css({
            width: animationReducedSize.width,
            height: animationReducedSize.height
        });

        presenter.configuration.dimensions = {
            preview: { width: previewReducedSize.width, height: previewReducedSize.height },
            animation: { width: animationReducedSize.width, height: animationReducedSize.height }
        };
    }

    function deferredQueueDecoratorChecker() {
        return presenter.isLoaded;
    }

    // This function returns string containing CSS declaration of elements
    // background image size in percentage measure
    function calculateBackgroundSize(size, framesCount, imageType) {
        var cssValue;

        switch(size) {
            case presenter.FRAME_SIZE.SCALED:
            case presenter.FRAME_SIZE.STRETCHED:
                cssValue = imageType === presenter.IMAGE_TYPE.ANIMATION ? (framesCount * 100) : 100;
                cssValue += '% 100%';
                break;
            default:
                cssValue = '';
        }

        return cssValue;
    }

    function previewImageLogic(previewImage) {
        var isScaledMode = presenter.configuration.frameSize === presenter.FRAME_SIZE.SCALED;
        var previewWidth = presenter.configuration.dimensions.preview.width;
        var previewHeight = presenter.configuration.dimensions.preview.height;

        $(previewImage).addClass('animation-hidden-image');
        $(presenter.DOMElements.viewContainer).append(previewImage);
        var previewDimensions = presenter.calculateContainerDimensions($(previewImage).width(), $(previewImage).height(), previewWidth, previewHeight);

        var previewBackgroundSize = calculateBackgroundSize(presenter.configuration.frameSize, presenter.configuration.framesCount, presenter.IMAGE_TYPE.PREVIEW);
        $(presenter.DOMElements.preview).css({
            'background-image': 'url(' + presenter.configuration.image + ')',
            width: isScaledMode ? previewDimensions.horizontal + 'px' : previewWidth + 'px',
            height: isScaledMode ? previewDimensions.vertical + 'px' : previewHeight + 'px'
        });

        if (previewBackgroundSize) {
            $(presenter.DOMElements.preview).css('background-size', previewBackgroundSize);
        }

        $(previewImage).remove();
    }

    function getCanvasFromImg(image) {
        var canvas = document.createElement('canvas');
        canvas.setAttribute('width', $(image).width());
        canvas.setAttribute('height', $(image).height());
        var ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0);

        return canvas;
    }

    function animationImageLogic(animationImage) {
        $(animationImage).addClass('animation-hidden-image');
        $(presenter.DOMElements.viewContainer).append(animationImage);

        var animationWidth = presenter.configuration.dimensions.animation.width;
        var animationHeight = presenter.configuration.dimensions.animation.height;
        var animationDimensions = presenter.calculateContainerDimensions($(animationImage).width() / presenter.configuration.framesCount, $(animationImage).height(), animationWidth, animationHeight);
        var animationBackgroundSize = calculateBackgroundSize(presenter.configuration.frameSize, presenter.configuration.framesCount, presenter.IMAGE_TYPE.ANIMATION);
        var source_width = (parseInt(presenter.configuration.oryginal_width, 10) ? presenter.configuration.oryginal_width : $(animationImage).width()) / presenter.configuration.framesCount;
        var source_height = parseInt(presenter.configuration.oryginal_height,10) ? presenter.configuration.oryginal_height : $(animationImage).height();
        var elementWidth;
        var elementHeight;

        switch (presenter.configuration.frameSize) {
            case presenter.FRAME_SIZE.ORIGINAL:
                elementWidth = source_width;
                elementHeight = source_height;
                break;
            case presenter.FRAME_SIZE.SCALED:
                elementWidth = animationDimensions.horizontal;
                elementHeight = animationDimensions.vertical;
                break;
            case presenter.FRAME_SIZE.STRETCHED:
                elementWidth = animationWidth;
                elementHeight = animationHeight;
                break;
        }
        elementWidth = Math.round(elementWidth);
        elementHeight = Math.round(elementHeight);
        var image = animationImage;

        //Repair bug with jpg in android
        if (["5.1.1", "5.0.2"].indexOf(window.MobileUtils.getAndroidVersion(navigator.userAgent)) &gt; -1) {
            image = getCanvasFromImg(animationImage);
        }

        presenter.images = [];
        var makeFrames = function() {
            var i;
            try {
                for (i = 0; i &lt; presenter.configuration.framesCount; i++) {
                    presenter.images.push(new presenter.ImageWrapper({
                        destinationHeight: elementHeight,
                        destinationWidth: elementWidth,
                        destinationX: 0,
                        destinationY: 0,
                        image: image,
                        sourceHeight: source_height,
                        sourceWidth: source_width,
                        sourceX: i * source_width,
                        sourceY: 0
                    }));
                }
            } catch (e) {
                if (e.name === "NS_ERROR_NOT_AVAILABLE") {
                    makeFrames();
                } else {
                    throw e;
                }
            }
        };
        makeFrames();

        var $animationDOM = $(presenter.DOMElements.animation);
        var clickhandler = $("&lt;div&gt;&lt;/div&gt;").css({"background":"transparent", 'width': elementWidth, 'height': elementHeight, 'position':'absolute'});
        $animationDOM.append(clickhandler);


        presenter.createCanvas(elementWidth, elementHeight);
        $animationDOM.append(presenter.canvas);


        $(presenter.DOMElements.animation).css({
            width: elementWidth + 'px',
            height: elementHeight + 'px'
        });

        if (animationBackgroundSize) {
            $(presenter.DOMElements.animation).css('background-size', animationBackgroundSize);
        }

        $(animationImage).remove();
    }

    presenter.createCanvas = function AddonAnimation_createCanvas(elementWidth, elementHeight) {
        presenter.canvas = document.createElement('canvas');
        presenter.canvas.setAttribute('width', elementWidth);
        presenter.canvas.setAttribute('height', elementHeight);

        presenter.canvasContext = presenter.canvas.getContext('2d');

        // draw first frame
        presenter.drawImage(
            presenter.canvasContext,
            presenter.images[0]
        );
    };

    presenter.drawImage = function AddonAnimation_drawImage(ctx, image) {
        drawImageIOSFix(
            ctx,
            image.image,
            image.sourceX,
            image.sourceY,
            image.sourceWidth,
            image.sourceHeight,
            image.destinationX,
            image.destinationY,
            image.destinationWidth,
            image.destinationHeight
        );
    };

    function loadImages() {
        showLoadingScreen();

        var img = $('&lt;img src="'+ presenter.configuration.animation +'"/&gt;').load(function() {
        	presenter.configuration.oryginal_width = this.width;
        	presenter.configuration.oryginal_height = this.height;
        	$(this).remove();
        });

        if (presenter.configuration.isPreview) {
            $.imgpreload([presenter.configuration.image], {
                all: function () {
                    previewImageLogic(this[0]);
                    hideLoadingScreen();
                }
            });
        } else {
            $.imgpreload([presenter.configuration.image, presenter.configuration.animation], {
                all: function() {
                    var isFirstPreview = $(this[0]).attr('src') == presenter.configuration.image;
                    var previewImage = isFirstPreview ? this[0] : this[1];
                    var animationImage = isFirstPreview ? this[1] : this[0];
    
                    previewImageLogic(previewImage);
                    imageLoadedCallback(animationImage);
                }
            });
        }
    }

    function imageLoadedCallback (image) {
        animationImageLogic(image);
        hideLoadingScreen();
        loadImagesEndCallback();
    }
    
    function prepareLoadingScreen(containerWidth, containerHeight) {
        if (presenter.configuration.isPreview) return;

        var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
        if (loadingSrc) presenter.DOMElements.loading.attr('src', loadingSrc);

        $(presenter.DOMElements.loading).css({
            top: ((containerHeight - $(presenter.DOMElements.loading).height()) / 2) + 'px',
            left: ((containerWidth - $(presenter.DOMElements.loading).width()) / 2) + 'px'
        });
    }

    function showLoadingScreen() {
        $(presenter.DOMElements.loading).show();
    }

    function hideLoadingScreen() {
        $(presenter.DOMElements.loading).hide();
    }

    function changeFrame() {
        var i = presenter.configuration.currentFrame;

        presenter.drawImage(presenter.canvasContext, presenter.images[i]);

        if (presenter.configuration.animationState === presenter.ANIMATION_STATE.STOPPED) {
            showLabelsForFrame(0);
        } else {
            showLabelsForFrame(presenter.configuration.currentFrame + 1);
        }
    }

    function prepareLabels() {
        var scaleX = 1.0;
        var scaleY = 1.0;
        if (presenter.configuration.baseDimensions.width != 0) {
            scaleX = presenter.configuration.dimensions.animation.width / presenter.configuration.baseDimensions.width;
        }
        if (presenter.configuration.baseDimensions.height != 0) {
            scaleY = presenter.configuration.dimensions.animation.height / presenter.configuration.baseDimensions.height;
        }

        for (var i = 0; i &lt; presenter.configuration.labels.count; i++) {
            var label = presenter.configuration.labels.content[i];
            var labelElement = document.createElement('span');

            $(labelElement).addClass('animation-label');
            $(labelElement).html(label.text);
            $(labelElement).css({
                top: label.top * scaleY,
                left: label.left * scaleX,
                visibility: 'hidden'
            });
            if (scaleX != 1.0 || scaleY != 1.0) {
                $(labelElement).css(generateTransformDict(scaleX, scaleY));
            }
            $(presenter.DOMElements.viewContainer).append(labelElement);
        }
    }

    function generateTransformDict(scaleX, scaleY) {
        var scale = "scale(" + scaleX + "," + scaleY + ")";
        return {
            'transform': scale,
            '-ms-transform': scale,
            '-webkit-transform': scale,
            '-o-transform': scale,
            '-moz-transform': scale,
           "-webkit-transform-origin": "top left",
           "-ms-transform-origin": "top left",
           "transform-origin": "top left"
        }
    }

    presenter.playAnimation = function() {
        $(presenter.DOMElements.preview).hide();
        $(presenter.DOMElements.animation).show();
        presenter.configuration.animationState = presenter.ANIMATION_STATE.PLAYING;
        showLabelsForFrame(presenter.configuration.currentFrame + 1);
        if (presenter.configuration.watermarkOptions.show) {
            $(presenter.DOMElements.watermark).hide();
        }
        presenter.configuration.watermarkOptions.clicked = true;
        $.doTimeout(presenter.configuration.queueName, presenter.configuration.frameDuration, presenter.onTimeoutCallback);
    };

    presenter.onTimeoutCallback = function AddonAnimation_onTimeoutCallback() {
        if (presenter.configuration.animationState !== presenter.ANIMATION_STATE.PLAYING) {
                return false;
        }

        if (presenter.configuration.currentFrame &lt; presenter.configuration.framesCount - 1) {
            presenter.configuration.currentFrame++;
            changeFrame();
        } else {
            if (presenter.configuration.loop || presenter.configuration.resetOnEnd) {
                presenter.configuration.currentFrame = 0;
                presenter.sendEndAnimationEvent();
                changeFrame();
            } else {
                presenter.configuration.animationState = presenter.ANIMATION_STATE.ENDED;
                $.doTimeout(presenter.configuration.queueName, false);
                presenter.endAnimationHandler();
                return false;
            }
        }

        if (presenter.configuration.currentFrame === 0 &amp;&amp; !presenter.configuration.loop) {
            if (presenter.configuration.resetOnEnd) {
                presenter.stop();
                return false;
            }
        }

        return true;
    };

    presenter.pause = deferredSyncQueue.decorate(function() {
        if (presenter.configuration.animationState !== presenter.ANIMATION_STATE.PLAYING) return;

        presenter.configuration.animationState = presenter.ANIMATION_STATE.PAUSED;
        if (presenter.configuration.watermarkOptions.show) {
            $(presenter.DOMElements.watermark).show();
        }
        presenter.configuration.watermarkOptions.clicked = false;
        $.doTimeout(presenter.configuration.queueName, true);
    });

    presenter.stop = deferredSyncQueue.decorate(function() {
        $(presenter.DOMElements.preview).show();
        $(presenter.DOMElements.animation).hide();
        presenter.configuration.animationState = presenter.ANIMATION_STATE.STOPPED;
        presenter.configuration.currentFrame = 0;
        changeFrame();
        if (presenter.configuration.watermarkOptions.show) {
            $(presenter.DOMElements.watermark).show();
        }
        presenter.configuration.watermarkOptions.clicked = false;
        $.doTimeout(presenter.configuration.queueName, false);
    });

    function elementClickHandler(e) {
        e.stopPropagation();

        switch (presenter.configuration.animationState) {
            case presenter.ANIMATION_STATE.PAUSED:
            case presenter.ANIMATION_STATE.STOPPED:
                presenter.playAnimation();
                break;

            case presenter.ANIMATION_STATE.PLAYING:
                presenter.pause();
                break;

            case presenter.ANIMATION_STATE.ENDED:
                presenter.stop();
                break;
        }
    }

    function handleMouseActions() {
        if (presenter.configuration.isClickDisabled) return;

        $(presenter.DOMElements.preview).click(elementClickHandler);
        $(presenter.DOMElements.animation).click(elementClickHandler);
        $(presenter.DOMElements.watermark).click(elementClickHandler);
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;

        presenter.eventBus = controller.getEventBus();
    };

    function presenterLogic(view, model, isPreview) {
        setDOMElementsHrefsAndSelectors(view);

        presenter.model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(presenter.model);
        presenter.configuration.isPreview = isPreview;

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.isLoaded = false;
        setElementsDimensions(view);
        prepareLoadingScreen(model.Width, model.Height);

        presenter.imagesLoadedDfd = new jQuery.Deferred();
        presenter.imagesLoaded = presenter.imagesLoadedDfd.promise();

        $.when(presenter.imagesLoaded).then(function () {
            presenter.isLoaded = true;

            deferredSyncQueue.resolve()
        });

        loadImages();
        prepareLabels();
        showLabelsForFrame(0);
    }

    presenter.createPreview = function(view, model){
        presenterLogic(view, model, true);
    };

    presenter.destroy = function AddonAnimation_destroy() {
        presenter.canvas = null;
        presenter.canvasContext = null;
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.reset = deferredSyncQueue.decorate(function(){
        this.stop();
        presenter.configuration.watermarkOptions.clicked = false;
        if(presenter.configuration.watermarkOptions.show) {
            $(presenter.DOMElements.watermark).show();
        } else {
            $(presenter.DOMElements.watermark).hide();
        }

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        if (!presenter.configuration.isVisibleByDefault) {
            presenter.hideLabels();
        }
    });

    presenter.getState = function() {
        if (!presenter.isLoaded) return '';

        if (presenter.ANIMATION_STATE.PLAYING === presenter.configuration.animationState) {
            presenter.pause();
        }

        return JSON.stringify({
            currentFrame: presenter.configuration.currentFrame,
            animationState: presenter.configuration.animationState,
            isVisible : presenter.configuration.isVisible,
            watermarkClicked : presenter.configuration.watermarkOptions.clicked
        });
    };

    presenter.setState = deferredSyncQueue.decorate(function(stateString) {
        if (!stateString) return;

        var state = JSON.parse(stateString);

        presenter.configuration.currentFrame = state.currentFrame;
        presenter.configuration.animationState = state.animationState;
        presenter.configuration.watermarkOptions.clicked = state.watermarkClicked;
        changeFrame();

        if (state.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        if (!presenter.configuration.watermarkOptions.clicked) {
            showLabelsForFrame(0);
        }

        //noinspection FallthroughInSwitchStatementJS
        switch (presenter.configuration.animationState) {
            case presenter.ANIMATION_STATE.PLAYING:
                presenter.playAnimation();
                break;
            case presenter.ANIMATION_STATE.PAUSED:
            case presenter.ANIMATION_STATE.ENDED:
                $(presenter.DOMElements.preview).hide();
                $(presenter.DOMElements.animation).show();
                break;
        }

        if (presenter.configuration.watermarkOptions.show &amp;&amp; !presenter.configuration.watermarkOptions.clicked) {
            $(presenter.DOMElements.watermark).show();
        } else {
            $(presenter.DOMElements.watermark).hide();
        }
    });

    function loadImagesEndCallback () {
        presenter.configuration.animationState = presenter.ANIMATION_STATE.PAUSED;
        presenter.configuration.currentFrame = 0;

        handleMouseActions();

        Watermark.draw(presenter.DOMElements.watermark, presenter.configuration.watermarkOptions);
        if (presenter.configuration.watermarkOptions.show &amp;&amp; !presenter.configuration.watermarkOptions.clicked) {
            $(presenter.DOMElements.watermark).show();
        } else {
            $(presenter.DOMElements.watermark).hide();
        }

        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        if (!presenter.configuration.isVisibleByDefault) {
            presenter.hideLabels();
        }

        presenter.imagesLoadedDfd.resolve();
    }

    presenter.play = deferredSyncQueue.decorate(function () {
        if (presenter.configuration.animationState === presenter.ANIMATION_STATE.ENDED) {
            presenter.stop();
        } else {
            presenter.playAnimation();
            if (presenter.playerController.isWCAGOn()) {
                isSpeaking = true;

                var speakCallback = function () {
                    isSpeaking = false;
                    presenter.endAnimationHandler();
                };

                presenter.speakWithDelay([window.TTSUtils.getTextVoiceObject(presenter.configuration.altText, presenter.configuration.lang)], speakCallback);
            }
        }
    });

    presenter.executeCommand = function(name, params) {
        var commands = {
            'stop': presenter.stop,
            'pause': presenter.pause,
            'play': presenter.play,
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        $(presenter.DOMElements.viewContainer).css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.hideLabels = function() {
        $(presenter.DOMElements.viewContainer).find('.animation-label').each(function () {
            $(this).css('visibility', 'hidden');
        });
    };

    presenter.hide = deferredSyncQueue.decorate(function() {
        this.configuration.isVisible = false;
        if(presenter.configuration.animationState == presenter.ANIMATION_STATE.PLAYING) {
            this.pause();
        }
        this.setVisibility(false);
        this.hideLabels();
    });

    presenter.show = deferredSyncQueue.decorate(function() {
        this.configuration.isVisible = true;
        if(presenter.configuration.animationState == presenter.ANIMATION_STATE.PLAYING) {
            this.playAnimation();
        }
        this.setVisibility(true);
        showLabelsForFrame(presenter.configuration.currentFrame + 1);
    });

    // This function validates and converts number from string representation to positive integer value
    presenter.sanitizePositiveNumber = function(number) {
        if (!number) {
            return {
                isError: true
            };
        }

        var parsedNumber = parseInt(number, 10);
        if (isNaN(parsedNumber)) {
            return {
                isError: true
            };
        }

        if (parsedNumber &lt; 0) {
            return {
                isError: true
            };
        }

        return {
            isError: false,
            number: parsedNumber
        };
    };

    // If validation error occurs then one of the following error codes are returned
    // L_01 - 'Text' value invalid
    // L_02 - 'Top' value invalid
    // L_03 - 'Left' value invalid
    // L_04 - Label empty
    presenter.validateLabels = function(labelsArray, framesCount) {
        var labels = { count: 0, content: [] };

        if (ModelValidationUtils.isArrayEmpty(labelsArray)) {
            return { isError: false, labels: labels };
        }

        for (var i = 0; i  &lt; labelsArray.length; i++) {
            if (ModelValidationUtils.isArrayElementEmpty(labelsArray[i])) {
                return { isError: true, errorCode: "L_04" };
            }

            if (ModelValidationUtils.isStringEmpty(labelsArray[i].Text)) {
                return { isError: true, errorCode: "L_01" };
            }

            var sanitizedTopPosition = presenter.sanitizePositiveNumber(labelsArray[i].Top);
            if (sanitizedTopPosition.isError) {
                return { isError: true, errorCode: "L_02" };
            }

            var sanitizedLeftPosition = presenter.sanitizePositiveNumber(labelsArray[i].Left);
            if (sanitizedLeftPosition.isError) {
                return { isError: true, errorCode: "L_03" };
            }

            if (ModelValidationUtils.isStringEmpty(labelsArray[i].Frames)) {
                labelsArray[i].Frames = "0";
            }

            var convertedFrames = ImageViewer.convertFramesList(labelsArray[i].Frames, 0, framesCount);
            if (convertedFrames.isError) {
                return { isError: true, errorCode: convertedFrames.errorCode};
            }

            var label = {
                text: labelsArray[i].Text,
                top: sanitizedTopPosition.number,
                left: sanitizedLeftPosition.number,
                frames: convertedFrames.list
            };

            labels.content.push(label);
            labels.count++;
        }

        return { isError: false, labels: labels };
    };

    presenter.validateFramesCount = function(framesCount) {
        var sanitizedFramesCount = presenter.sanitizePositiveNumber(framesCount);

        if (sanitizedFramesCount.isError) {
            return {
                isError: true,
                errorCode: "FC_01"
            };
        }

        return {
            isError: false,
            framesCount: sanitizedFramesCount.number
        };
    };

    presenter.validateFrameDuration = function(frameDuration) {
        var sanitizedFrameDuration = presenter.sanitizePositiveNumber(frameDuration);

        if (sanitizedFrameDuration.isError) {
            return {
                isError: true,
                errorCode: "FD_01"
            };
        }

        return {
            isError: false,
            frameDuration: sanitizedFrameDuration.number
        };
    };

    presenter.validateFrameSize = function(frameSize) {
        var result;

        switch (frameSize) {
            case "Keep aspect ratio":
                result = presenter.FRAME_SIZE.SCALED;
                break;
            case "Stretch":
                result = presenter.FRAME_SIZE.STRETCHED;
                break;
            default:
                result = presenter.FRAME_SIZE.ORIGINAL;
                break;
        }

        return result;
    };

    presenter.validateModel = function(model) {
        var speechTexts = presenter.getspeechTexts(model['speechTexts']);

        if (ModelValidationUtils.isStringEmpty(model["Preview image"])) {
            return { isError: true, errorCode: "PI_01" };
        }

        if (ModelValidationUtils.isStringEmpty(model.Animation)) {
            return { isError: true, errorCode: "AI_01" };
        }

        var validatedFramesCount = presenter.validateFramesCount(model["Frames count"]);
        if (validatedFramesCount.isError) {
            return {
                isError: true,
                errorCode: validatedFramesCount.errorCode
            };
        }

        var validatedFrameDuration = presenter.validateFrameDuration(model["Frame duration"]);
        if (validatedFrameDuration.isError) {
            return {
                isError: true,
                errorCode: validatedFrameDuration.errorCode
            };
        }

        var validatedLabels = presenter.validateLabels(model.Labels, validatedFramesCount.framesCount);
        if (validatedLabels.isError) {
            return {
                isError: true,
                errorCode: validatedLabels.errorCode
            };
        }

        var watermarkOptions = {
            "color": model["Watermark color"],
            "opacity": model["Watermark opacity"],
            "size": model["Watermark size"]
        };
        var validatedOptions = Watermark.validateOptions(watermarkOptions);
        validatedOptions.show = ModelValidationUtils.validateBoolean(model["Show watermark"]);
        validatedOptions.clicked = false;

        var isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        var baseDimensions = {
            width: 0,
            height: 0
        };

        if (model['Base width'].trim().length !== 0) {
            var validatedBaseWidth = ModelValidationUtils.validatePositiveInteger(model['Base width']);
            if (validatedBaseWidth.isValid) {
                baseDimensions.width = validatedBaseWidth.value;
            } else {
                validatedBaseWidth.errorCode = 'ID_01';
                validatedBaseWidth.isError = true;
                return validatedBaseWidth;
            }
        }
        if (model['Base height'].trim().length !== 0) {
            var validatedBaseHeight = ModelValidationUtils.validatePositiveInteger(model['Base height']);
            if (validatedBaseHeight.isValid) {
                baseDimensions.height = validatedBaseHeight.value;
            } else {
                validatedBaseHeight.errorCode = 'ID_01';
                validatedBaseHeight.isError = true;
                return validatedBaseHeight;
            }
        }

        return {
            isError: false,
            queueName: model.ID,
            image: model["Preview image"],
            animation: model.Animation,
            framesCount: validatedFramesCount.framesCount,
            frameDuration: validatedFrameDuration.frameDuration,
            loop: ModelValidationUtils.validateBoolean(model.Loop),
            labels: validatedLabels.labels,
            frameSize: presenter.validateFrameSize(model["Frame size"]),
            resetOnEnd: !ModelValidationUtils.validateBoolean(model["Don't reset on end"]),
            isClickDisabled: ModelValidationUtils.validateBoolean(model["Is click disabled"]),
            isVisibleByDefault: isVisibleByDefault,
            isVisible: isVisibleByDefault,
            watermarkOptions: validatedOptions,
            addonID: model.ID,
            altText: model['Alternative Text'],
            altTextPreview: model['Preview Alternative Text'],
            lang: model['langAttribute'],
            speechTexts: speechTexts,
            baseDimensions: baseDimensions
        };
    };

    presenter.getLabelIndexesForFrame = function (labels, frame) {
        var indexes = [];

        if(!labels) return indexes;

        for (var i = 0, length = labels.length; i &lt; length; i++) {
            if (labels[i].frames.indexOf(frame) !== -1) {
                indexes.push(i);
            }
        }

        return indexes;
    };

    presenter._internal = {
        deferredSyncQueue: deferredSyncQueue
    };

    function showLabelsForFrame(frame) {
        var labels = presenter.configuration.labels.content,
            indexes = presenter.getLabelIndexesForFrame(labels, frame);

        presenter.hideLabels();

        if (!presenter.configuration.isVisible) return;

        for (var i = 0, length = indexes.length; i &lt; length; i++) {
            $(presenter.DOMElements.viewContainer).find('.animation-label:eq(' + indexes[i] + ')').css('visibility', 'visible');
        }
    }

    // This function is from https://github.com/stomita/ios-imagefile-megapixel
    function detectVerticalSquash(img) {
        if (!navigator.userAgent.match(/(iPad|iPhone|iPod touch);.*CPU.*OS 7_\d/i)){
            return 1;
        }

        try {
            var iw = img.naturalWidth, ih = img.naturalHeight;
            var canvas = document.createElement('canvas');
            canvas.width = 1;
            canvas.height = ih;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            var data = ctx.getImageData(0, 0, 1, ih).data;
            // search image edge pixel position in case it is squashed vertically.
            var sy = 0;
            var ey = ih;
            var py = ih;
            while (py &gt; sy) {
                var alpha = data[(py - 1) * 4 + 3];
                if (alpha === 0) {
                    ey = py;
                } else {
                    sy = py;
                }
                py = (ey + sy) &gt;&gt; 1;
            }
            var ratio = (py / ih);
        }
        catch (err) {
            // we expect Security error on SVG files
            return 1;
        }
        return (ratio === 0) ? 1 : ratio;
    }

    function drawImageIOSFix(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) {
        var vertSquashRatio = detectVerticalSquash(img);
        ctx.clearRect(0, 0, presenter.canvas.width, presenter.canvas.height);
        ctx.drawImage(img, sx * vertSquashRatio, sy * vertSquashRatio,
            sw * vertSquashRatio, sh * vertSquashRatio,
            dx, dy, dw, dh );
    }

    presenter.sendEndAnimationEvent = function () {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'ended',
            'score': ''
        };
        
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.endAnimationHandler = function () {
        if (presenter.configuration.animationState == presenter.ANIMATION_STATE.ENDED &amp;&amp;
            (!presenter.playerController.isWCAGOn() || !isSpeaking)) {
            presenter.sendEndAnimationEvent();
            if (presenter.playerController.isWCAGOn()) {
                presenter.stop();
            }
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function(data, callback) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; presenter.playerController.isWCAGOn()) {
            tts.speakWithCallback(data, callback);
        }
    };

    var delayedSpeakInterval = null; // only used by speakWithDelay, which is why they are here and not at the top of the file
    var delayedSpeakTimeout = null;
    //This method works like speak, except that it waits for TTS to be idle instead of interrupting it
    presenter.speakWithDelay = function (data, callback) {
        presenter.stopDelayedTTS();

        function setSpeakInterval (data, callback) {
            delayedSpeakInterval = setInterval(function () {
                var speechSynthSpeaking = false;
                var responsiveVoiceSpeaking = false;

                // Detect if TTS is idle
                if ('speechSynthesis' in window) {
                    speechSynthSpeaking = window.speechSynthesis.speaking;
                }
                if (window.responsiveVoice) {
                    responsiveVoiceSpeaking = window.responsiveVoice.isPlaying();
                }

                if (!speechSynthSpeaking &amp;&amp; !responsiveVoiceSpeaking) {
                    // If TTS is idle, pass data to TTS and break the loop
                    clearInterval(delayedSpeakInterval);
                    delayedSpeakInterval = null;
                    var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
                    if (tts &amp;&amp; presenter.playerController.isWCAGOn()) {
                        tts.speakWithCallback(data, callback);
                    }
                }
            }, 200);
        }

        /*
        * The timeout is used to ensure that if animation is triggered by another addon,
        * that addon has the opportunity to use TTS first, since animation acts as feedback
        */
        delayedSpeakTimeout = setTimeout(function(){ setSpeakInterval(data, callback); }, 300);
    };

    presenter.stopDelayedTTS = function() {
        if(delayedSpeakTimeout) {
            clearTimeout(delayedSpeakTimeout);
            delayedSpeakTimeout = null;
        }
         if(delayedSpeakInterval) {
            clearInterval(delayedSpeakInterval);
            delayedSpeakInterval = null;
        }
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {

        var keys = {
            ENTER: 13,
            SPACE: 32
        };

        var enter = function() {
            event.preventDefault();
            presenter.stopDelayedTTS();
            if(!isShiftKeyDown) {
                presenter.speak([window.TTSUtils.getTextVoiceObject(presenter.configuration.altTextPreview, presenter.configuration.lang)]);
            }
        };

        var space = function() {
            event.preventDefault();
            presenter.stopDelayedTTS();
            if (presenter.configuration.animationState == presenter.ANIMATION_STATE.PLAYING ||
                presenter.configuration.animationState == presenter.ANIMATION_STATE.ENDED ||
                isSpeaking) {
                presenter.stop();
                presenter.speak([window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.stop)]);
                isSpeaking = false;
            } else {
                presenter.stop();
                presenter.play();
            }
        };

        var mapping = {};
        mapping[keys.ENTER] = enter;
        mapping[keys.SPACE] = space;

        try {
            mapping[keycode]();
        } catch (er) {
        }
    };

    presenter.isEnterable = function() {return false};

    presenter.markerWCAG = {}; // This is a marker identifying the addon as supporting WCAG to the editor

    /**
     *
     * @param configuration {object}
     * @param configuration.image source image
     * @param configuration.sourceX {number} source x position
     * @param configuration.sourceY {number} source y position
     * @param configuration.sourceWidth {number} source width
     * @param configuration.sourceHeight {number} source height
     * @param configuration.destinationWidth {number} destination width
     * @param configuration.destinationHeight {number} destination height
     * @param configuration.destinationX {number} where source should be placed in destination x position
     * @param configuration.destinationY {number} where source should be placed in destination y position
     * @constructor
     */
    presenter.ImageWrapper = function (configuration) {
        this.image = configuration.image;
        this.sourceX = configuration.sourceX;
        this.sourceY = configuration.sourceY;
        this.sourceWidth = configuration.sourceWidth;
        this.sourceHeight = configuration.sourceHeight;
        this.destinationWidth = configuration.destinationWidth;
        this.destinationHeight = configuration.destinationHeight;
        this.destinationX = configuration.destinationX;
        this.destinationY = configuration.destinationY;
    };

    return presenter;
}
</presenter></addon><addon id="Assessments_Navigation_Bar">
    <model>
        <property name="Sections" nameLabel="Assessments_Navigation_Bar_property_sections" type="text"/>
        <property displayName="Real-time navigationbar-page-attempted" name="addClassAreAllAttempted" nameLabel="Assessments_Navigation_Bar_property_add_class_are_all_attempted" type="boolean"/>
        <property displayName="Number of buttons" name="userButtonsNumber" nameLabel="Assessments_Navigation_Bar_property_number_of_buttons" type="string"/>
        <property displayName="Width of buttons" name="userButtonsWidth" nameLabel="Assessments_Navigation_Bar_property_width_of_buttons" type="string"/>
        <property displayName="Default order" name="defaultOrder" nameLabel="Assessments_Navigation_Bar_property_default_order" type="boolean"/>
        <property displayName="Use dynamic pagination" name="useDynamicPagination" nameLabel="Assessments_Navigation_Bar_property_use_dynamic_pagination" type="boolean"/>
        <property displayName="Expandable ANB sections" name="enableDropdownPagesList" nameLabel="Assessments_Navigation_Bar_property_enable_dropdown_pages_list" type="boolean"/>
        <property displayName="Enable redirect to page" name="enableRedirectToPage" nameLabel="Assessments_Navigation_Bar_property_enable_redirect_to_page" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Assessments_Navigation_Bar_property_speech_texts" type="staticlist">
            <property name="PreviousPage" nameLabel="Assessments_Navigation_Bar_speech_text_previous_page" type="staticrow">
                <property name="PreviousPage" nameLabel="Assessments_Navigation_Bar_speech_text_previous_page" type="string"/>
            </property>
            <property name="ShowPreviousPages" nameLabel="Assessments_Navigation_Bar_speech_text_show_previous_pages" type="staticrow">
                <property name="ShowPreviousPages" nameLabel="Assessments_Navigation_Bar_speech_text_show_previous_pages" type="string"/>
            </property>
            <property name="Title" nameLabel="Assessments_Navigation_Bar_speech_text_title" type="staticrow">
                <property name="Title" nameLabel="Assessments_Navigation_Bar_speech_text_title" type="string"/>
            </property>
            <property name="GoToPage" nameLabel="Assessments_Navigation_Bar_speech_text_go_to_page" type="staticrow">
                <property name="GoToPage" nameLabel="Assessments_Navigation_Bar_speech_text_go_to_page" type="string"/>
            </property>
            <property name="ShowNextPages" nameLabel="Assessments_Navigation_Bar_speech_text_show_next_pages" type="staticrow">
                <property name="ShowNextPages" nameLabel="Assessments_Navigation_Bar_speech_text_show_next_pages" type="string"/>
            </property>
            <property name="NextPage" nameLabel="Assessments_Navigation_Bar_speech_text_next_page" type="staticrow">
                <property name="NextPage" nameLabel="Assessments_Navigation_Bar_speech_text_next_page" type="string"/>
            </property>
        </property>
    </model>
<css>.assessments-navigation-bar-wrapper .navigation-buttons-first .previous {
    width: 40px;
}

.assessments-navigation-bar-wrapper .navigation-buttons-first {
    display: inline-block;
    height: 80%;
}

.assessments-navigation-bar-wrapper .navigation-buttons-last {
    display: inline-block;
    height: 80%;
}

.assessments-navigation-bar-wrapper .inactive {
    background-color: #80BBC8;
    color: #C0C0C0;
}

.assessments-navigation-bar-wrapper {
    background-color: #F3F3F3;
}

.assessments-navigation-bar-wrapper .sections {
    display: inline-block;
    height: 100%;
}

.assessments-navigation-bar-wrapper .sections.enable-dropdown {
    display: flex;
    flex-direction: row;
    height: 100%;
    gap: 10px;
}

.assessments-navigation-bar-wrapper .section {
    display: inline-block;
    height: 100%;
}

.assessments-navigation-bar-wrapper .section.expandable-section {
    position: relative;
    display: flex;
    align-items: center;
    cursor: pointer;
}

.assessments-navigation-bar-wrapper .section_name {
    text-align: center;
    text-wrap: nowrap;
    height: 20%;
}

.assessments-navigation-bar-wrapper .element {
    display: inline-block;
    text-align: center;
    height: 100%;
}

.assessments-navigation-bar-wrapper .buttons.dropdown-list .element {
    display: grid;
    text-align: center;
    width: 100%;
    height: 100%;
}

.assessments-navigation-bar-wrapper .buttons {
    height: 80%;
}

.assessments-navigation-bar-wrapper .buttons.dropdown-list {
    width: 100%;
    height: auto !important;
    top: 40px;
    display: none;
    flex-direction: column;
    position: absolute;
    background-color: beige;
}

.assessments-navigation-bar-wrapper .button_text {
    position: relative;
    top: 50%;
    transform: translateY(-50%);
}

.assessments-navigation-bar-wrapper .mouse-over {
    background-color: #7F7F7F;
    color: #F3F3F3;
    cursor: pointer;
}

.assessments-navigation-bar-wrapper .current_page {
    background-color: #2091AB;
    color: white;
}

.assessments-navigation-bar-wrapper .bookmark {
    color: red;
}

@media screen and (max-width: 450px) {
    .addon_Assessments_Navigation_Bar.enabled-page-list {
        width: 400px !important;
        overflow-x: scroll;
        overflow-y: hidden;
        background: none;
        box-shadow: none;
        scroll-behavior: smooth;
        z-index: 1;
    }

    /* hide scroll bar*/
    .addon_Assessments_Navigation_Bar.enabled-page-list::-webkit-scrollbar {
        display: none;
    }
}
</css><view>&lt;div class="assessments-navigation-bar-wrapper"&gt;
    &lt;div class="navigation-buttons-first"&gt;
        &lt;div class="previous"&gt;&amp;hellip;&lt;/div&gt;
        &lt;div class="turn_backward"&gt;&amp;lt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="sections"&gt;
        &lt;div class ="section_0"&gt;
            &lt;div class="section_name"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="navigation-buttons-last"&gt;
          &lt;div class="turn_forward"&gt;&amp;hellip;&lt;/div&gt;
          &lt;div class="next"&gt;&amp;gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="assessments-navigation-bar-wrapper"&gt;
    &lt;div class="navigation-buttons-first"&gt;
        &lt;div class="previous"&gt;&amp;hellip;&lt;/div&gt;
        &lt;div class="turn_backward"&gt;&amp;lt;&lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="sections"&gt;
        &lt;div class ="section_0"&gt;
            &lt;div class="section_name"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="navigation-buttons-last"&gt;
          &lt;div class="turn_forward"&gt;&amp;hellip;&lt;/div&gt;
          &lt;div class="next"&gt;&amp;gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonAssessments_Navigation_Bar_create(){

    var presenter = function(){};

    presenter.isWCAGOn = false;
    presenter.SECTION_NAME_HEIGHT = 20;
    presenter.isFirstEnter = true;
    presenter.isPageListNavigationActive = false;
    presenter.currentSelectedChapterIndex = -1;
    presenter.currentSelectedChapterName = '';
    presenter.previousSectionsBarHeight = 0;

    presenter.ERROR_MESSAGES = {
        S_00: "Section property cant be empty string",
        S_01: "Pages range are invalid on section %section% in sections property. Numbers have to be integers only.",
        S_02: "Pages range is invalid on section: %section% in sections property.",
        S_03: "Pages descriptions are invalid on section: %section% in sections property. Number of descriptions is too small.",
        S_04: "Pages descriptions are invalid on section: %section% in sections property. Number of descriptions is too big.",
        S_05: "Section: %section_1% pages numbers cant intersect with page numbers of section: %section_2%.",
        S_06: "Number of buttons property can't be equal or below 0.",
        S_07: "Number of buttons property have to be an integer",
        S_08: "Number of buttons can't be greater than number of pages in sections",
        S_09: "Buttons width property can't be equal or below 0.",
        S_10: "Buttons width property have to be an integer",
        S_11: "Pages CSS classes are invalid on section: %section% in sections property. Number of CSS classes is too small.",
        S_12: "Pages CSS classes are invalid on section: %section% in sections property. At least one of CSS classes is invalid.",
        S_13: "Static position value in section %section% is invalid: the value must be 'left', 'right', or left empty."
    };

    presenter.DEFAULT_TTS_PHRASES = {
        PreviousPage: "Go to previous page",
        ShowPreviousPages: "Show previous pages",
        Title: "Title",
        GoToPage: "Go to page",
        ShowNextPages: "Show next pages",
        NextPage: "Go to next page",
    };

    presenter.CSS_CLASSES = {
        ALL_ATTEMPTED: "all-attempted",
        PREVIOUS: "previous",
        BUTTON_TEXT: "button_text",
        SECTION_NAME: "section_name",
        NEXT: "next",
        BUTTON: "button",
        ELEMENT: "element",
        TURN_BACK: "turn_back",
        TURN_FORWARD: "turn_forward",
        CURRENT_PAGE: "current_page",
        BOOKMARK: "bookmark",
        INACTIVE: "inactive",
        ENABLED_PAGE_LIST: "enabled-page-list",
        CURRENT_SECTION: "current-section",
        SELECTED: "selected",
        VISIBLE_PAGE_LIST: "visible-page-list",
    };

    presenter.attemptedButtons = [];

    presenter.configuration = {
        isValid: undefined,
        addonID: undefined,
        sections: undefined,
        addClassAreAllAttempted: undefined,
        userButtonsNumber: undefined,
        userButtonsWidth: undefined,
        numberOfButtons: undefined,
        navigationLeftIndex: 0,
        navigationRightIndex: 0,
        numberOfPages: 0,
        defaultOrder: false
    };

    presenter.keyboardControllerObject = null;
    //this field is set based on the metadata. It overrides the defaultOrder property
    // If set to false it prevents state import
    presenter.randomizeLesson = null;

    presenter.ASSESSMENT_USER_TYPES = {
        NONE: 0,
        TEACHER: 1,
        STUDENT: 2,
    }
    presenter.assessmentUser = presenter.ASSESSMENT_USER_TYPES.NONE;

    presenter.showErrorMessage = function(message, substitutions) {
        var errorContainer;
        if(typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for(var key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.$view.html(errorContainer);
    };

    function getErrorObject (errorCode, errorData) {
        return {
            isValid: false,
            errorCode: errorCode,
            errorData: errorData
        };
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.presentation = controller.getPresentation();
        var currentIndex = presenter.playerController.getCurrentPageIndex();
        var mappings = presenter.playerController.getPagesMapping();
        mappings.forEach(function (value, index) {
            if (value === currentIndex) {
                presenter.currentPageIndex = index;
            }
        });
        var context = controller.getContextMetadata();
         if (context != null) {
            if ("randomizeLesson" in context) {
                 presenter.randomizeLesson = context["randomizeLesson"];
            }
            if ("assessmentUser" in context) {
               if (context["assessmentUser"] == "teacher") {
                    presenter.assessmentUser = presenter.ASSESSMENT_USER_TYPES.TEACHER;
               } else if (context["assessmentUser"] == "student") {
                    presenter.assessmentUser = presenter.ASSESSMENT_USER_TYPES.STUDENT;
               }
            }
         }
        presenter.commander = controller.getCommands();
        presenter.eventBus = controller.getEventBus();
        presenter.addEventListeners();
    };

    presenter.addEventListeners = function () {
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('ValueChanged', this);
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.changeToPage = function (index) {
        var mappings = presenter.playerController.getPagesMapping();
        var i = mappings[index];
        if (index !== presenter.currentPageIndex) {
            presenter.commander.gotoPageIndex(i);
        }
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'bookmarkCurrentPage': presenter.bookmarkCurrentPage,
            'removeBookmark' : presenter.removeBookmark,
            'moveToPage': presenter.moveToPageCommand,
            'moveToPreviousPage': presenter.moveToPreviousPage,
            'moveToNextPage': presenter.moveToNextPage
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.moveToPreviousPage = function() {
        presenter.navigationManager.goLeft();
    };

    presenter.moveToNextPage = function() {
        presenter.navigationManager.goRight();
    };

    presenter.moveToPageCommand = function (params) {
        presenter.moveToPage(params[0]);
    };

    presenter.moveToPage = function (pageIndex) {
        var validatedPageIndex = presenter.validatePageIndex(pageIndex);

        if (validatedPageIndex.isValid) {
            var page = presenter.sections.getPageByIndex(validatedPageIndex.index);

            presenter.changeToPage(page.page);
        }
    };

    presenter.validatePageIndex = function (pageIndex) {
        var parsedIndex = Number(pageIndex);

        if (isNaN(parsedIndex)) {
            return getErrorObject();
        }

        if (presenter.isFloat(parsedIndex)) {
            return getErrorObject();
        }

        if ((parsedIndex &lt; 1) || (parsedIndex &gt; presenter.configuration.numberOfPages)) {
            return getErrorObject();
        }

        return {
            isValid: true,
            index: (parsedIndex - 1)
        };
    };

    presenter.bookmarkCurrentPage = function () {
        presenter.sections.setBookmarkCurrentPage(true);
        presenter.navigationManager.bookmarkCurrentButton();
    };

    presenter.removeBookmark = function () {
        presenter.sections.setBookmarkCurrentPage(false);
        presenter.navigationManager.removeBookmarkFromCurrentButton();
    };

    // Fisher-Yates algorithm
    // based on http://sedition.com/perl/javascript-fy.html
    function shuffleArray (a) {
        var i = a.length;
        if ( i == 0 ) return [];

        while ( --i ) {
            var j = Math.floor( Math.random() * ( i + 1 ) );
            var tempi = a[i];
            a[i] = a[j];
            a[j] = tempi;
        }

        return a;
    }

    presenter.Button = function (viewDescription, customCssClassNames, isPageButton) {
        this.description = viewDescription;
        this.customCssClassNames = customCssClassNames;
        this.isPageButton = isPageButton;
        this.$view = this.createView();
        this.$view_text;
        this.actualCommand;
        this.navigateToPage;
        this.isActualButton = false;

        this.connectEvents();
    };

    presenter.Button.prototype.connectEvents = function () {
        this.$view[0].addEventListener("click", this, false);
        this.$view[0].addEventListener("mouseover", this, false);
        this.$view[0].addEventListener("mouseout", this, false);
    };

    presenter.Button.prototype.addCssClass = function (cssClass) {
        this.$view.addClass(cssClass);
    };

    presenter.Button.prototype.removeCssClass = function (cssClass) {
        this.$view.removeClass(cssClass);
    };

    presenter.Button.prototype.setCommand = function (command) {
        this.actualCommand = command;
    };

    presenter.Button.prototype.setNavigateToPage = function (navigateToPage) {
        this.navigateToPage = navigateToPage;
    };

    presenter.Button.prototype.execute = function () {
        this.actualCommand();
    };

    presenter.Button.prototype.handleEvent = function (event) {
        if (presenter.isPreview) {
            return;
        }

        switch (event.type) {
            case "click":
                this.execute();
                break;
            case "mouseover":
                this.$view.addClass("mouse-over");
                break;
            case "mouseout":
                this.$view.removeClass("mouse-over");
                break;
        }

        event.preventDefault();
        event.stopPropagation();
    };

    presenter.Button.prototype.setAsCurrent = function () {
        this.isActualButton = true;
        this.$view.addClass(presenter.CSS_CLASSES.CURRENT_PAGE);
    };

    presenter.Button.prototype.addBookmark = function () {
        this.$view.addClass(presenter.CSS_CLASSES.BOOKMARK);
    };

    presenter.Button.prototype.removeBookmark = function () {
        this.$view.removeClass(presenter.CSS_CLASSES.BOOKMARK);
    };

    presenter.Button.prototype.removeAttempted = function () {
        this.$view.removeClass(presenter.CSS_CLASSES.ALL_ATTEMPTED);
    };

    presenter.Button.prototype.createView = function () {
        var $view = $('&lt;div&gt;&lt;/div&gt;');
        const elementWidth = presenter.configuration.enableDropdownPagesList &amp;&amp; this.isPageButton ? '100%' : `${presenter.configuration.sizes.elementWidth}px`;

        $view.css({
            width: elementWidth
        });

        this.$view_text = $('&lt;div&gt;&lt;/div&gt;');
        this.$view_text.text(this.description);
        this.$view_text.addClass(presenter.CSS_CLASSES.BUTTON_TEXT);

        if (presenter.configuration.enableDropdownPagesList &amp;&amp; this.isPageButton) {
            this.$view_text.css({width: elementWidth});
        }

        $view.append(this.$view_text);
        $view.addClass(presenter.CSS_CLASSES.BUTTON);
        $view.addClass(presenter.CSS_CLASSES.ELEMENT);

        if (this.customCssClassNames &amp;&amp; this.customCssClassNames.length !== 0) {
            this.customCssClassNames.map((cssClassName) =&gt; {
                $view.addClass(cssClassName);
            })
        }

        return $view;
    };

    presenter.Button.prototype.getView = function () {
        return this.$view;
    };

    presenter.Button.prototype.addInactiveClass = function () {
        this.$view.addClass(presenter.CSS_CLASSES.INACTIVE);
    };

    presenter.Button.prototype.removeInactiveClass = function () {
        this.$view.removeClass(presenter.CSS_CLASSES.INACTIVE);
    };

    presenter.NavigationButtonLeft = function () {
        presenter.Button.call(this, "&lt;");
        this.$view.removeClass(presenter.CSS_CLASSES.BUTTON);
        this.$view.addClass(presenter.CSS_CLASSES.PREVIOUS);
        this.setCommand(function () {
            presenter.navigationManager.goLeft();
        });
    };

    presenter.NavigationButtonLeft.prototype = Object.create(presenter.Button.prototype);
    presenter.NavigationButtonLeft.constructor = presenter.NavigationButtonLeft;

    presenter.NavigationButtonRight = function () {
        presenter.Button.call(this, "&gt;");
        this.$view.addClass(presenter.CSS_CLASSES.NEXT);
        this.$view.removeClass(presenter.CSS_CLASSES.BUTTON);
        this.setCommand(function () {
            presenter.navigationManager.goRight();
        });
    };

    presenter.NavigationButtonRight.prototype = Object.create(presenter.Button.prototype);
    presenter.NavigationButtonRight.constructor = presenter.NavigationButtonRight;

    presenter.HellipButton = function (hellipFunction, className) {
        presenter.Button.call(this, "&amp;hellip;"); // ...
        this.$view_text.html(this.description);
        this.$view.removeClass(presenter.CSS_CLASSES.BUTTON);
        this.$view.addClass(className);

        this.setCommand(hellipFunction);
    };

    presenter.HellipButton.prototype = Object.create(presenter.Button.prototype);
    presenter.HellipButton.constructor = presenter.HellipButton;

    presenter.Page = function (page, view_description, sectionName, sectionCssClass, buttonCssClassNames, staticPosition) {
        this.description = view_description;
        this.page = page;
        this.isBookmarkOn = false;
        this.sectionName = sectionName;
        this.sectionCssClass = sectionCssClass;
        this.buttonCssClassNames = buttonCssClassNames;
        if (staticPosition === undefined) {
            this.staticPosition = '';
        } else {
            this.staticPosition = staticPosition;
        }
    };

    presenter.Page.prototype.setBookmarkOn = function (bookmark) {
        this.isBookmarkOn = bookmark;
    };

    presenter.Page.prototype.getSectionClassName = function () {
        return this.sectionCssClass;
    };

    presenter.Page.prototype.getSectionName = function () {
        return this.sectionName;
    };

    presenter.Page.prototype.getChangeToPageCommand = function () {
        return function () {
            var index = Number(this);
            presenter.changeToPage(index);
        }.bind(this.page);
    };

    presenter.Page.prototype.isActualPage = function () {
        return presenter.currentPageIndex === this.page;
    };

    presenter.Section = function (pages, sectionName, pagesDescriptions, sectionCssClassName, buttonsCssClassNames, staticPosition) {
        this.name = sectionName;
        this.cssClassName = sectionCssClassName;
        this.buttonsCssClassNames = buttonsCssClassNames;
        this.staticPosition = staticPosition;
        this.pages = this.createPages(pages, pagesDescriptions);
    };

    presenter.Section.prototype.createPages = function (pages, pagesDescriptions) {
        var keepDefaultOrder = presenter.configuration.defaultOrder;
        if (presenter.assessmentUser != presenter.ASSESSMENT_USER_TYPES.NONE) {
            if (presenter.assessmentUser == presenter.ASSESSMENT_USER_TYPES.TEACHER) {
                keepDefaultOrder = true;
            } else {
                // presenter.assessmentUser set to student
                keepDefaultOrder = false;
            }
        } else if (presenter.randomizeLesson != null) {
            keepDefaultOrder = !presenter.randomizeLesson;
        }

        if (isTestGeneratorPreview()) {
            keepDefaultOrder = true;
        }

        var pagesToCreate = keepDefaultOrder ? pages : shuffleArray(pages);

        return pagesToCreate.map(function (page, index) {
            if (page == -1) return null;
            return new presenter.Page(
                page, pagesDescriptions[index], this.name,
                this.cssClassName, this.buttonsCssClassNames,
                this.staticPosition
            );
        }, this).filter(function(page) {
            return page != null;
        });
    };

    // keep default page order in test generator preview
    function isTestGeneratorPreview() {
        if (!presenter.playerController || presenter.playerController.isPlayerInCrossDomain()) {
            return false;
        }
        const parentUrl = window.parent?.location.href;
        return parentUrl.includes('lesson/view/assessment') &amp;&amp; parentUrl.includes('preview');
    }

    presenter.Sections = function (sections) {
        this.sections = this.createSections(sections);
        this.allPages = this.getAllPagesInOrder(this.sections);
        this.attemptedPages = [];
        this.pagesIndexes = this.getPagesIndexes(this.allPages);
    };

    presenter.Sections.prototype.markPageAsAttempted = function (page) {
        if(!(this.attemptedPages.indexOf(page) &gt; -1)){
            this.attemptedPages.push(page);
        }
    };

    presenter.Sections.prototype.markPageAsNotAttempted = function (page) {
        var attempted_page_index = this.attemptedPages.indexOf(page);
        if (attempted_page_index !== -1) {
            this.attemptedPages.splice(attempted_page_index, 1);
        }
    };

    presenter.Sections.prototype.markAllPagesAsNotAttempted = function () {
        this.attemptedPages = [];
    };

    presenter.Sections.prototype.getPagesIndexes = function (pages) {
        return pages.map(function (page) {
            return page.page;
        });
    };

    presenter.Sections.prototype.getPageByIndex = function (index) {
        return this.allPages[index];
    };

    presenter.Sections.prototype.isHomeCurrentPage = function () {
        return this.allPages[0].isActualPage();
    };

    presenter.Sections.prototype.isLastCurrentPage = function () {
        return this.allPages[this.allPages.length - 1].isActualPage();
    };

    presenter.Sections.prototype.getActualPageIndex = function () {
        for (var i = 0; i &lt; this.allPages.length; i++) {
            if (this.allPages[i].isActualPage()) {
                return i;
            }
        }

        return undefined;
    };

    presenter.Sections.prototype.doesActualPageExists = function () {
        return this.pagesIndexes.indexOf(presenter.currentPageIndex) !== -1;
    };

    presenter.Sections.prototype.getNextPageToCurrent = function () {
        var pageIndex = this.getActualPageIndex();

        if ((pageIndex !== undefined) &amp;&amp; (pageIndex &lt; presenter.configuration.numberOfPages - 1)) {
            pageIndex = pageIndex + 1;
            return this.getPageByIndex(pageIndex);
        }

        return undefined;
    };

    presenter.Sections.prototype.getPreviousPageToCurrent = function () {
        var pageIndex = this.getActualPageIndex();

        if ((pageIndex !== undefined) &amp;&amp; (pageIndex &gt; 0)) {
            pageIndex = pageIndex - 1;
            return this.getPageByIndex(pageIndex);
        }

        return undefined;
    };

    presenter.Sections.prototype.getAllPagesInOrder = function (sections) {
        return sections.reduce(function (result, section) {
            return result.concat(section.pages);
        }, []);
    };

    presenter.Sections.prototype.addClassAllAttemptedToPage = function (current_page_index) {
        presenter.navigationManager.markCurrentButtonWithAttemptedClass();
        presenter.sections.markPageAsAttempted(current_page_index);
    };

    presenter.Sections.prototype.removeClassAllAttemptedToPage = function (current_page_index) {
        presenter.navigationManager.removeCurrentButtonAttemptedClass();
        presenter.sections.markPageAsNotAttempted(current_page_index);
    };

    presenter.Sections.prototype.setBookmarkCurrentPage = function (bookmark) {
        for (var i = 0; i &lt; this.allPages.length; i++) {
            if (this.allPages[i].isActualPage()) {
                this.allPages[i].isBookmarkOn = bookmark;
                break
            }
        }
    };

    presenter.Sections.prototype.getPages = function (leftIndex, numberOfPages) {
        var pages = [];

        var standardPages = [];
        for (var i = 0; i &lt; this.allPages.length; i++) {
            if (this.allPages[i].staticPosition === undefined || this.allPages[i].staticPosition.length === 0) {
                standardPages.push(this.allPages[i]);
            }
        }
        if (!presenter.configuration.enableDropdownPagesList) {
            if (leftIndex + numberOfPages &gt;= standardPages.length) {
                leftIndex = ((standardPages.length) - numberOfPages);
            }

            if (leftIndex &lt; 0) {
                leftIndex = 0;
            }

            for (var i = leftIndex; i &lt; standardPages.length; i++) {
                if (numberOfPages == 0) {
                    break;
                }

                pages.push(standardPages[i]);
                numberOfPages--;
            }
        } else {
            var actualSections = []
            for (var i = 0; i &lt; presenter.configuration.sections.length; i++) {
                if (presenter.configuration.sections[i].sectionName.length &gt; 0) {
                    actualSections.push(presenter.configuration.sections[i]);
                }
            }
            if (leftIndex + numberOfPages &gt;= actualSections.length) {
                leftIndex = actualSections.length - numberOfPages;
            }

            if (leftIndex &lt; 0) {
                leftIndex = 0;
            }
            var sectionNames = [];

            for (var i = leftIndex; i &lt; actualSections.length; i++) {
                if (numberOfPages == 0) {
                    break;
                }
                sectionNames.push(actualSections[i].sectionName);
                numberOfPages--;
            }
            for (var i = 0; i &lt; standardPages.length; i++) {
                if (sectionNames.indexOf(standardPages[i].getSectionName()) != -1) {
                    pages.push(standardPages[i]);
                }
            }
        }
        return pages;
    };

    presenter.Sections.prototype.getStaticPages = function () {
        var pages = [];
        for (var i = 0; i &lt; this.allPages.length; i++) {
            if (this.allPages[i].staticPosition &amp;&amp; this.allPages[i].staticPosition.length &gt; 0) {
                pages.push(this.allPages[i]);
            }
        }
        return pages;
    }

    presenter.filterSectionsWithTooManyPages = function(sections) {
        var mapping = presenter.playerController.getPagesMapping();

        if (presenter.assessmentUser == presenter.ASSESSMENT_USER_TYPES.TEACHER) {
            for (var i = 0; i &lt; sections.length; i++) {
                for(var j = 0; j &lt; sections[i].pages.length; j++) {
                    var page = sections[i].pages[j];
                    if (mapping[page] == -1) {
                        sections[i].pages[j] = -1;
                    }
                }
            }
        } else {
            for (var i = 0; i &lt; sections.length; i++) {
                sections[i].pages = sections[i].pages.filter(function (page) {
                    return mapping[page] &gt;= 0;
                });
            }
        }

        sections = sections.filter(function(section) {
            return section.pages.length &gt; 0;
        });

        return sections;
    };

    presenter.Sections.prototype.createSections = function (sections) {
        if (presenter.playerController) {
            sections = presenter.filterSectionsWithTooManyPages(sections);
        }

        return sections.map(
            function (section, index) {
                var sectionCssClass = "section_" + index;
                return new presenter.Section(
                    section.pages, section.sectionName,
                    section.pagesDescriptions, sectionCssClass,
                    section.sectionButtonsCssClassNames,
                    section.staticPosition
                );
        });
    };

    presenter.NavigationManager = function () {
        this.leftSideIndex = 0;
        this.actualSections = [];
        this.actualSectionsNames = [];
        this.$navigationButtonsFirst;
        this.$navigationButtonsLast;
        this.navigationButtonLeft;
        this.navigationButtonRight;
        this.$sections;
        this.buttons = [];
        this.shiftCount = 0;
        this.leftOffset = 0;
        this.nextPrevBtnWasClicked = false;

        this.initView();
    };

    presenter.NavigationManager.prototype.isBackButtonVisible = function () {
        return presenter.$wrapper.find("." + presenter.CSS_CLASSES.TURN_BACK).length;
    };

    presenter.NavigationManager.prototype.getNumberOfNavElements = function () {
        return this.isBackButtonVisible() ? 4 : 3;
    };

    presenter.NavigationManager.prototype.calculateLeftOffset = function (previousLeftSideValue, nextPrevBtnWasClicked) {
        const currentIndex = presenter.playerController.getCurrentPageIndex();
        const staticPages = presenter.configuration.numberOfStaticPages;
        const navElements = this.getNumberOfNavElements();
        const rightSideIndex = previousLeftSideValue + presenter.configuration.numberOfButtons - navElements - staticPages;
        const isBackButtonVisible = this.isBackButtonVisible();

        if (rightSideIndex === (currentIndex + 1)) {
            this.setLeftOffset(1);
        }

        // situation when there are static pages and show next page was clicked
        if (rightSideIndex === currentIndex &amp;&amp; nextPrevBtnWasClicked &amp;&amp; !isBackButtonVisible) {
            this.setLeftOffset(1);
        }

        if (previousLeftSideValue === currentIndex) {
            this.setLeftOffset(0);
        }
    };

    presenter.NavigationManager.prototype.setLeftOffset = function (leftOffset) {
        this.leftOffset = leftOffset;
    };

    presenter.NavigationManager.prototype.getLeftSideIndex = function () {
        return this.leftSideIndex;
    };

    presenter.NavigationManager.prototype.setLeftSideIndex = function (previousLeftSideIndex, previousLeftSideValue) {
        if (!presenter.configuration.useDynamicPagination) {
            return;
        }

        const navElements = this.getNumberOfNavElements();
        const currentIndex = presenter.playerController.getCurrentPageIndex();
        const numberOfButtonsInShift = presenter.configuration.numberOfButtons - navElements;
        const actualCurrentIndex = this.getLeftIndex(currentIndex);
        if (previousLeftSideIndex &gt;= this.actualPages.length || previousLeftSideIndex &lt; 0) {
            previousLeftSideIndex = this.getLeftIndex(previousLeftSideValue);
        }

        if (this.staticPages.length) {
            if (currentIndex &lt;= previousLeftSideValue) {
                this.leftSideIndex = actualCurrentIndex - 1;
                this.setIndexToFirst(this.leftSideIndex);
            } else {
                this.leftSideIndex = previousLeftSideIndex;
            }
            this.shiftCount = Math.floor((this.leftSideIndex + 1) / numberOfButtonsInShift);
        }

        if (this.staticPages.length === 0) {
            const staticPages = presenter.configuration.numberOfStaticPages;
            const rightSideIndex = previousLeftSideValue + presenter.configuration.numberOfButtons - navElements - staticPages;
            if (currentIndex &lt;= previousLeftSideValue) {
                const actualCurrentIndex = this.getLeftIndex(currentIndex);
                this.leftSideIndex = actualCurrentIndex - 1;
                this.setIndexToFirst(this.leftSideIndex);
                this.setLeftOffset(0);
            } else if (currentIndex - rightSideIndex &gt;= -1) {
                this.setLeftOffset(1);
            } else {
                this.leftSideIndex = previousLeftSideIndex;
                this.setLeftOffset(0);
            }

            this.shiftCount = Math.floor((this.leftSideIndex + 1) / numberOfButtonsInShift);
        }
    };

    presenter.NavigationManager.prototype.setIndexToFirst = function (index) {
        if (index !== 1) { return; }
        try {
            const page = presenter.sections.allPages[1];
            const _page = this.actualPages[index];
            if (page.description === _page.description) {
                this.leftSideIndex = 0;
            }
        } catch (e) {}
    };

    presenter.NavigationManager.prototype.getLeftIndex = function (previousLeftSideValue) {
        return this.actualPages.findIndex((page) =&gt; page.page === previousLeftSideValue);
    };

    presenter.NavigationManager.prototype.restartLeftSideIndex = function () {
        this.leftSideIndex = 0;
        this.shiftCount = 0;
    };

    presenter.NavigationManager.prototype.goRight = function () {
        this.nextPrevBtnWasClicked = true;
        var page = presenter.sections.getNextPageToCurrent();

        if (page) {
            presenter.changeToPage(page.page);
        }
    };

    presenter.NavigationManager.prototype.goLeft = function () {
        this.nextPrevBtnWasClicked = true;
        var page = presenter.sections.getPreviousPageToCurrent();

        if (page) {
            presenter.changeToPage(page.page);
        }
    };

    presenter.NavigationManager.prototype.bookmarkCurrentButton = function () {
        var currentButton = this.buttons.filter(function (element) {
            return element.isActualButton;
        })[0];
        if (currentButton !== undefined) currentButton.addBookmark();
    };

    presenter.NavigationManager.prototype.removeBookmarksFromButtons = function () {
        this.buttons.forEach(function(button) {
            button.removeBookmark();
        });

        presenter.sections.allPages.map(function (page) {
            page.isBookmarkOn = false;
        });
    };

    presenter.NavigationManager.prototype.removeBookmarkFromCurrentButton = function () {
        var currentButton = this.buttons.filter(function (element) {
            return element.isActualButton;
        })[0];
        if (currentButton !== undefined) currentButton.removeBookmark();
    };

    presenter.NavigationManager.prototype.markButtonsWithAttempted = function (attemptedPages) {
        this.buttons.filter(function (button) {
            return (attemptedPages.indexOf(button.navigateToPage) !== -1);
        }).forEach(function (button) {
            button.addCssClass(presenter.CSS_CLASSES.ALL_ATTEMPTED);
        });
    };

    presenter.NavigationManager.prototype.removeAttemptedFromButtons = function () {
        this.buttons.forEach(function(button) {
            button.removeAttempted();
        });
        presenter.sections.markAllPagesAsNotAttempted();
    };

    presenter.NavigationManager.prototype.initView = function () {
        this.appendNavigationButtonsFirst();
        this.appendSectionsContainer();
        this.appendNavigationButtonsLast();
    };

    presenter.NavigationManager.prototype.appendSectionsContainer = function () {
        this.$sections = $('&lt;div&gt;&lt;/div&gt;');

        this.$sections.addClass("sections");
        presenter.configuration.enableDropdownPagesList &amp;&amp; this.$sections.addClass("enable-dropdown");

        presenter.$wrapper.append(this.$sections);
    };

    presenter.NavigationManager.prototype.getActualButtonsArray = function () {
        return this.buttons.filter(function (button) {
            return button.isActualButton
        });
    };

    presenter.NavigationManager.prototype.markCurrentButtonWithAttemptedClass = function () {
        var button = this.getActualButtonsArray();

        if (button.length === 1) {
            button[0].addCssClass(presenter.CSS_CLASSES.ALL_ATTEMPTED);
        }
    };

    presenter.NavigationManager.prototype.removeCurrentButtonAttemptedClass = function () {
        var button = this.getActualButtonsArray();

        if (button.length === 1) {
            button[0].removeCssClass(presenter.CSS_CLASSES.ALL_ATTEMPTED);
        }
    };

    presenter.NavigationManager.prototype.appendNavigationButtonsFirst = function () {
        const $navigationButtonsFirst = $('&lt;div&gt;&lt;/div&gt;');
        $navigationButtonsFirst.addClass("navigation-buttons-first");

        this.navigationButtonLeft = new presenter.NavigationButtonLeft();
        $navigationButtonsFirst.append(this.navigationButtonLeft.getView());

        this.$navigationButtonsFirst = $navigationButtonsFirst;
        presenter.$wrapper.append(this.$navigationButtonsFirst);
    };

    presenter.NavigationManager.prototype.appendNavigationButtonsLast = function () {
        const $navigationButtonsLast = $('&lt;div&gt;&lt;/div&gt;');
        $navigationButtonsLast.addClass("navigation-buttons-last");

        this.navigationButtonRight = new presenter.NavigationButtonRight();
        $navigationButtonsLast.append(this.navigationButtonRight.getView());

        this.$navigationButtonsLast = $navigationButtonsLast;
        presenter.$wrapper.append(this.$navigationButtonsLast);
    };

    presenter.NavigationManager.prototype.clearStateAndButtons = function () {
        this.removeSections();
        this.removeHellips();
        this.removeInactiveClassFromNavigationButtons();
        this.buttons = [];
        this.actualPages = [];
        this.staticPages = [];
    };

    presenter.NavigationManager.prototype.removeInactiveClassFromNavigationButtons = function () {
        this.navigationButtonLeft.removeInactiveClass();
        this.navigationButtonRight.removeInactiveClass();
    };

    presenter.NavigationManager.prototype.addLeftHellip = function () {
        if (this.shouldAddLeftHellip()) {
            this.getHellip($.fn.append.bind(this.$navigationButtonsFirst), this.shiftPagesToLeft.bind(this), presenter.CSS_CLASSES.TURN_BACK);
            return 1;
        }

        return 0;
    };

    presenter.NavigationManager.prototype.addRightHellip = function () {
        if (this.shouldAddRightHellip()) {
            this.rightHellip = this.getHellip($.fn.prepend.bind(this.$navigationButtonsLast), this.shiftPagesToRight.bind(this), presenter.CSS_CLASSES.TURN_FORWARD);
            return 1;
        }
        return 0;
    };

    presenter.NavigationManager.prototype.calculateNumberOfPages = function (hellipsCount) {
        var number = presenter.configuration.numberOfButtons - 2 - hellipsCount;
        if (number &lt; 1) {
            return 1;
        } else {
            return number;
        }
    };

    presenter.NavigationManager.prototype.getHellip = function (containerAddFunction, hellipFunction, className) {
        var button = new presenter.HellipButton(hellipFunction, className);

        containerAddFunction(button.getView());

        return button;
    };

    presenter.NavigationManager.prototype.setSections = function () {
        this.clearStateAndButtons();

        this.hellipsCount = 0;

        this.hellipsCount += this.addLeftHellip();
        this.hellipsCount += this.addRightHellip();

        this.addSections(this.calculateNumberOfPages(this.hellipsCount));

        if (presenter.configuration.enableDropdownPagesList) {
            var navButtonWidth = Math.floor((presenter.$wrapper.width() - this.$sections.width()) / (2 + this.hellipsCount));
            this.$navigationButtonsFirst.find('.element').css('width', navButtonWidth+'px');
            this.$navigationButtonsLast.find('.element').css('width', navButtonWidth+'px');
        }
    };

    presenter.NavigationManager.prototype.deactivateNavigationButtons = function () {
        if (presenter.sections.isHomeCurrentPage()) {
            this.navigationButtonLeft.addInactiveClass();
            return;
        }

        if (presenter.sections.isLastCurrentPage()) {
            this.navigationButtonRight.addInactiveClass();
        }
    };

    presenter.NavigationManager.prototype.removeSections = function () {
        presenter.$wrapper.find(".sections").children().remove();
        this.actualSections = [];
        this.actualSectionsNames = [];
    };

    presenter.NavigationManager.prototype.moveToCurrentPage = function () {
        if (presenter.sections.doesActualPageExists()) {
            this.moveToCurrentPageLogic();
        }
    };

    presenter.NavigationManager.prototype.moveToCurrentPageLogic = function () {
        var allPages = this.actualPages.concat(this.staticPages);
        for (var i = 0; i &lt; allPages.length; i++) {
            var page = allPages[i];
            if (page != undefined &amp;&amp; page.isActualPage != undefined &amp;&amp; page.isActualPage()) {
                return;
            }
        }

        if (this.leftOffset === 1 &amp;&amp; presenter.configuration.useDynamicPagination) {
            this.shiftPagesToRight(1);
            this.moveToCurrentPage();
        } else if (this.rightHellip) {
            this.shiftPagesToRight();
            this.moveToCurrentPage();
        }
    };

    presenter.NavigationManager.prototype.removeHellips = function () {
        presenter.$wrapper.find("." + presenter.CSS_CLASSES.TURN_FORWARD).remove();
        presenter.$wrapper.find("." + presenter.CSS_CLASSES.TURN_BACK).remove();
        delete this.rightHellip;
    };

    presenter.NavigationManager.prototype.shiftPagesToLeft = function () {
        var staticPagesNumber = presenter.configuration.numberOfStaticPages;
        if (this.shiftCount === 1) {
            this.leftSideIndex = 0;
        } else {
            if (presenter.configuration.numberOfButtons - 4 - staticPagesNumber&gt; 0) {
                this.leftSideIndex -=  (presenter.configuration.numberOfButtons - 4 - staticPagesNumber);
            } else {
                this.leftSideIndex -= 1;
            }
        }
        this.leftOffset = 0;

        if (this.shiftCount &gt; 0) {
            this.shiftCount--;
        } else {
            this.leftSideIndex = 0;
        }
        this.setSections();
    };

    presenter.NavigationManager.prototype.shiftPagesToRight = function (shift = 0) {
        if (shift === 0) {
            shift = this.getShiftToRight();
            this.shiftCount++;
        } else {
            this.shiftCount = Math.floor((presenter.sections.getActualPageIndex() + 1) / (presenter.configuration.numberOfButtons - 4));
        }

        this.leftSideIndex += shift;
        this.setSections();
    };

    presenter.NavigationManager.prototype.getShiftToRight = function () {
        let shift;
        if (presenter.configuration.userButtonsNumber) {
            shift = (presenter.configuration.numberOfButtons - 2 - this.hellipsCount);
        } else {
            shift = this.getNormalRightShift();
        }

        shift = shift - presenter.configuration.numberOfStaticPages;
        if (shift &lt;= 0) {
            shift = 1;
        }

        return shift;
    };

    presenter.NavigationManager.prototype.getNormalRightShift = function () {
        if (this.shiftCount === 0) {
            return (presenter.configuration.numberOfButtons - 3);
        } else {
            return (presenter.configuration.numberOfButtons - 4);
        }
    };

    presenter.NavigationManager.prototype.setSectionWidth = function ($section) {
        var numOfButtons = $section.find(".buttons").children().length;

        if (presenter.configuration.enableDropdownPagesList) {
            const elementWidth = `${presenter.configuration.sizes.elementWidth}px`;
            $section.css({ 'min-width': elementWidth });
        } else {
            const elementWidth = `${numOfButtons * presenter.configuration.sizes.elementWidth}px`;
            $section.css({ 'width': elementWidth });
        }
    };

    presenter.NavigationManager.prototype.setButtonCurrentPage = function (button, page) {
        if (page.isActualPage()) {
            button.setAsCurrent();
            const _self = this;
            setTimeout(function () {
                _self.updateVisiblePages(button);
            }, 0);
         }
    };

    presenter.NavigationManager.prototype.updateVisiblePages = function (button) {
        if (presenter.configuration.useDynamicPagination &amp;&amp; this.leftOffset === 1 &amp;&amp; this.isLastVisibleElement(button)) {
            this.shiftPagesToRight(1);
        }
    };

    presenter.NavigationManager.prototype.isLastVisibleElement = function (button) {
        const lastVisiblePage = this.actualPages[(this.actualPages.length - 1)].description;
        const lastPageIndex = presenter.sections.allPages.slice(-1).page;
        const isForwardButton = presenter.$wrapper.find("." + presenter.CSS_CLASSES.TURN_FORWARD).length &gt; 0;

        return lastVisiblePage === button.description &amp;&amp; button.navigateToPage !== lastPageIndex &amp;&amp; isForwardButton;
    };

    presenter.NavigationManager.prototype.setButtonBookmark = function (button, page) {
        if (page.isBookmarkOn) {
            button.addBookmark();
        }
    };

    presenter.NavigationManager.prototype.setButtonProperties = function (button, page) {
        if (presenter.isPreview) {
            return;
        }
        this.setButtonCurrentPage(button, page);
        this.setButtonBookmark(button, page);
    };

    presenter.NavigationManager.prototype.addSections = function (numberOfPages) {
        this.staticPages = presenter.sections.getStaticPages();
        this.actualPages = presenter.sections.getPages(this.leftSideIndex, numberOfPages - this.staticPages.length);
        var sectionIterator = -1;

        var len = this.actualPages.length;
        for (var i = 0; i &lt; len; i++) {
            var button = this.getPageButton(this.actualPages[i], true);
            this.setButtonProperties(button, this.actualPages[i]);
            this.buttons.push(button);

            sectionIterator = this.addButtonToSection(button, sectionIterator, this.actualPages[i]);
        }

        this.appendSectionsToView();
        this.appendStaticPages(this.staticPages);
        this.deactivateNavigationButtons();
    };

    presenter.NavigationManager.prototype.appendSectionsToView = function () {
        this.actualSections.forEach(function ($section) {
            this.setSectionWidth($section);
            this.$sections.append($section);
        }, this);
    };

    presenter.NavigationManager.prototype.appendStaticPages = function (pages) {
        presenter.$view.find('.navigation-buttons-first .section').remove();
        presenter.$view.find('.navigation-buttons-last .section').remove();
        var staticSections = [];
        for (var i = pages.length - 1; i &gt; -1; i--) {
            var page = pages[i];
            var sectionName = page.getSectionName();
            var $section = null;
            if (staticSections.indexOf(sectionName) == -1) {
                $section = this.getSection(sectionName, page.getSectionClassName());
                staticSections[sectionName] = $section;
                if (page.staticPosition == 'left') {
                    presenter.$view.find('.navigation-buttons-first .previous').after($section);
                } else {
                    if (presenter.$view.find('.navigation-buttons-last .section').length == 0) {
                        presenter.$view.find('.navigation-buttons-last .next').before($section);
                    } else {
                        presenter.$view.find('.navigation-buttons-last .section').first().before($section);
                    }
                }
            } else {
                $section = staticSections[sectionName];
            }


            var button = this.getPageButton(page);
            this.setButtonProperties(button, pages[i]);
            $section.find('.buttons').append(button.getView());
        };
        if (pages.length &gt; 0) {
            presenter.$view.find('.navigation-buttons-first &gt; div').css('float', 'left');
            presenter.$view.find('.navigation-buttons-last &gt; div').css('float', 'left');
        }
    }

    presenter.NavigationManager.prototype.getPageButton = function (page, isPageButton = false) {
        var button = new presenter.Button(page.description, page.buttonCssClassNames, isPageButton);
        button.setCommand(page.getChangeToPageCommand());
        button.setNavigateToPage(page.page);

        return button;
    };

    presenter.NavigationManager.prototype.addButtonToSection = function (button, sectionIterator, page) {
        if (page.getSectionClassName() === this.actualSectionsNames[sectionIterator]) {
            this.appendButtonToSection(button, sectionIterator);
        } else {
            this.addNewSection(page);
            sectionIterator++;
            this.appendButtonToSection(button, sectionIterator);
        }

        return sectionIterator
    };

    presenter.NavigationManager.prototype.appendButtonToSection = function (button, sectionIterator) {
        this.actualSections[sectionIterator].find(".buttons").append(button.getView());
    };

    presenter.NavigationManager.prototype.addNewSection = function (page) {
        var $section = this.getSection(page.getSectionName(), page.getSectionClassName());
        this.actualSectionsNames.push(page.getSectionClassName());
        this.actualSections.push($section);
    };

    presenter.NavigationManager.prototype.getSection = function (sectionName, cssClass) {
        var $section = $('&lt;div&gt;&lt;/div&gt;');
        $section.addClass(cssClass);
        $section.addClass("section");

        var $sectionName = $('&lt;div&gt;&lt;/div&gt;');
        $sectionName.text(sectionName);
        $sectionName.addClass(presenter.CSS_CLASSES.SECTION_NAME);

        if (presenter.configuration.enableDropdownPagesList) {
            $section.addClass("expandable-section");
            presenter.addClickListener($section, cssClass);
        }

        var $sectionButtons = $('&lt;div&gt;&lt;/div&gt;');
        $sectionButtons.addClass("buttons");
        if (presenter.configuration.enableDropdownPagesList) {
            const elementWidth = `${presenter.configuration.sizes.elementWidth}px`;
            $sectionButtons.addClass("dropdown-list");
            $sectionButtons.css('min-width', elementWidth);
        }

        $section.append($sectionName);
        $section.append($sectionButtons);

        if (presenter.configuration.enableDropdownPagesList &amp;&amp; !sectionName) {
            $section.css('display', 'none');
        }

        return $section;
    };

    presenter.NavigationManager.prototype.shouldAddLeftHellip = function () {
        return this.leftSideIndex &gt; 0;
    };

    presenter.NavigationManager.prototype.shouldAddRightHellip = function () {
        var buttonsWithoutNavigation = presenter.configuration.numberOfButtons - 2;
        var numberOfElements = presenter.configuration.enableDropdownPagesList ? getVisibleSectionsNumber() : presenter.configuration.numberOfPages;
        var leftSideIndex = this.leftSideIndex;
        if (leftSideIndex &lt; 0) leftSideIndex = 0;
        if (presenter.configuration.userButtonsNumber) {
            if (presenter.configuration.userButtonsNumber == 1) {
                return leftSideIndex + buttonsWithoutNavigation - this.hellipsCount &lt; numberOfElements - 1;
            }
        }
        return leftSideIndex + buttonsWithoutNavigation - this.hellipsCount &lt; numberOfElements;
    };

    presenter.addClickListener = function ($section, sectionClassName) {
        $section.on('click', function (event) {
            if (presenter.shouldRedirectToPage(sectionClassName)) {
                presenter.handleRedirectToPage(sectionClassName);
                return;
            }
            const isPageListVisible = presenter.isPageListOpen(sectionClassName);
            presenter.hideVisiblePageList();

            if (!isPageListVisible) {
                presenter.changePageListVisibility(sectionClassName);
            }
            event.preventDefault();
            event.stopPropagation();
            
            presenter.scrollToSelectedChapter();
        });
    };

    presenter.shouldRedirectToPage = function (sectionClassName) {
        const pageNumberInSection = $(`.${sectionClassName}`).find('.buttons.dropdown-list').children().length;

        return presenter.configuration.enableRedirectToPage &amp;&amp; pageNumberInSection === 1;
    }

    presenter.handleRedirectToPage = function (sectionClassName) {
        const selectedSection = presenter.getSelectedSection(sectionClassName);
        const pageIndex = selectedSection[0].pages[0].page;

        presenter.changeToPage(pageIndex);
    }

    presenter.getSelectedSection = function (sectionClassName) {
        return presenter.sections.sections.filter(section =&gt; section.cssClassName === sectionClassName);
    }

    presenter.changePageListVisibility = function (sectionClassName) {
        const $parentElement = $(`.${sectionClassName}`);
        const $buttonsContainer = $parentElement.find('.buttons');
        const isNotVisible = $buttonsContainer.css('display') === 'none';

        if (isNotVisible) {
            presenter.showPageListContainer($buttonsContainer, $parentElement);
            presenter.currentSelectedChapterName = sectionClassName;
            presenter.changeSectionsBarHeight(false);
        } else {
            presenter.hidePageListContainer($parentElement);
            presenter.changeSectionsBarHeight(true);
        }

        presenter.handleSettingCurrentSectionClass();
    };

    presenter.isPageListOpen = function (sectionClassName) {
        return $(`.${sectionClassName}`).hasClass(presenter.CSS_CLASSES.SELECTED);
    }

    presenter.showPageListContainer = function ($buttonsContainer, $parentElement) {
        $buttonsContainer.css('display', 'flex');
        $buttonsContainer.addClass(presenter.CSS_CLASSES.VISIBLE_PAGE_LIST);
        $parentElement.addClass(presenter.CSS_CLASSES.SELECTED);
    }

    presenter.hidePageListContainer = function ($parentElement) {
        const buttonsContainer = $(`.${presenter.CSS_CLASSES.VISIBLE_PAGE_LIST}`);

        buttonsContainer.removeClass(presenter.CSS_CLASSES.VISIBLE_PAGE_LIST);
        buttonsContainer.css('display', 'none');
        $parentElement.removeClass(presenter.CSS_CLASSES.SELECTED);
    }

    presenter.hideVisiblePageList = function () {
        if (!presenter.currentSelectedChapterName) {
            return;
        }

        const pageListContainer = $(`.${presenter.CSS_CLASSES.VISIBLE_PAGE_LIST}`);
        const $parentElement = $(`.${presenter.currentSelectedChapterName}`);

        if (pageListContainer) {
            $parentElement.removeClass(presenter.CSS_CLASSES.SELECTED);
            pageListContainer.removeClass(presenter.CSS_CLASSES.VISIBLE_PAGE_LIST);
            pageListContainer.css('display', 'none');
        }

        presenter.changeSectionsBarHeight(true);
    }
    
    presenter.scrollToSelectedChapter = function () {
        if (!presenter.isMobile()) {
            return;
        }

        $('.addon_Assessments_Navigation_Bar').get(0).scrollLeft = presenter.getLeftScroll();
    }
    
    presenter.isMobile = function () {
        return MobileUtils.isMobileUserAgent(navigator.userAgent) || MobileUtils.isEventSupported('touchend');
    }

    presenter.getScale = function () {
        const boundingWidth = $('#content').get(0).getBoundingClientRect().width;
        const elementWidth = +$('#content').css('width').replace('px', '');

        return boundingWidth / elementWidth;
    }

    presenter.getLeftScroll = function () {
        const selectedChapterPosition = $('.expandable-section.selected').offset();
        const currentScrollXBarPosition = $('.addon_Assessments_Navigation_Bar').get(0).scrollLeft;
        const scale = presenter.getScale();
        const offset = 20;
        const scrollLeft = (selectedChapterPosition.left / scale) + currentScrollXBarPosition - offset;

        return scrollLeft &lt; 0 ? 0 : scrollLeft;
    }

    presenter.changeSectionsBarHeight = function (setDefaultValue) {
        if (!presenter.isMobile()) {
            return;
        }

        const $addonElement = $('.addon_Assessments_Navigation_Bar');

        if (setDefaultValue) {
            $addonElement.css('height', presenter.previousSectionsBarHeight)
        } else {
            presenter.previousSectionsBarHeight = +$addonElement.css('height').replace('px', '');
            const pagesListHeight = +$(`.${presenter.CSS_CLASSES.VISIBLE_PAGE_LIST}`).css('height').replace('px', '');
            const totalHeight = presenter.previousSectionsBarHeight + pagesListHeight;
            $addonElement.css('height', totalHeight)
        }
    }

    presenter.handleSettingCurrentSectionClass = function () {
        const $element = $(`.${presenter.CSS_CLASSES.CURRENT_SECTION}`);
        if ($element) {
            $element.removeClass(presenter.CSS_CLASSES.CURRENT_SECTION);
        }

        presenter.addClassToCurrentSection();
    }

    presenter.addClassToCurrentSection = function () {
        const $element = $('.button.element.current_page');
        const $sectionElement = $element?.parent().parent();

        if ($sectionElement) {
            $sectionElement.addClass(presenter.CSS_CLASSES.CURRENT_SECTION);
        }
    }

    presenter.run = function(view, model){
        presenter.isPreview = false;
        presenter.runLogic(view, model);
    };

    presenter.createPreview = function (view, model) {
        presenter.isPreview = true;
        presenter.currentPageIndex = 0;
        presenter.runLogic(view, model);
    };

    function deleteCommands () {
        delete presenter.setState;
        delete presenter.getState;
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeNumberAndWidthOfButtons(model);
        upgradedModel = presenter.upgradeDefaultOrder(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeUseDynamicPagination(upgradedModel);
        upgradedModel = presenter.upgradeEnableDropdownPagesList(upgradedModel);
        upgradedModel = presenter.upgradeEnableRedirectToPage(upgradedModel);

        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.upgradeNumberAndWidthOfButtons = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.userButtonsWidth == undefined) {
            upgradedModel["userButtonsWidth"] = "";
        }

        if(model.userButtonsNumber == undefined) {
            upgradedModel["userButtonsNumber"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeDefaultOrder = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.defaultOrder === undefined) {
            upgradedModel["defaultOrder"] = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!model['speechTexts']) {
            upgradedModel['speechTexts'] = {
                PreviousPage: {PreviousPage: ""},
                ShowPreviousPages: {ShowPreviousPages: ""},
                Title: {Title: ""},
                GoToPage: {GoToPage: ""},
                ShowNextPages: {ShowNextPages: ""},
                NextPage: {NextPage: ""},
            }
        }

        return upgradedModel;
    };

    presenter.upgradeUseDynamicPagination = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!model.hasOwnProperty('useDynamicPagination')) {
            upgradedModel['useDynamicPagination'] = 'False';
        }

        return upgradedModel;
    }

    presenter.upgradeEnableDropdownPagesList = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!model.hasOwnProperty('enableDropdownPagesList')) {
            upgradedModel['enableDropdownPagesList'] = 'False';
        }

        return upgradedModel;
    }

    presenter.upgradeEnableRedirectToPage = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!model.hasOwnProperty('enableRedirectToPage')) {
            upgradedModel['enableRedirectToPage'] = 'False';
        }

        return upgradedModel;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            PreviousPage: presenter.DEFAULT_TTS_PHRASES.PreviousPage,
            ShowPreviousPages: presenter.DEFAULT_TTS_PHRASES.ShowPreviousPages,
            Title: presenter.DEFAULT_TTS_PHRASES.Title,
            GoToPage: presenter.DEFAULT_TTS_PHRASES.GoToPage,
            ShowNextPages: presenter.DEFAULT_TTS_PHRASES.ShowNextPages,
            NextPage: presenter.DEFAULT_TTS_PHRASES.NextPage,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            PreviousPage: presenter.getSpeechTextProperty(
                speechTexts.PreviousPage.PreviousPage,
                presenter.speechTexts.PreviousPage),
            ShowPreviousPages: presenter.getSpeechTextProperty(
                speechTexts.ShowPreviousPages.ShowPreviousPages,
                presenter.speechTexts.ShowPreviousPages),
            Title: presenter.getSpeechTextProperty(
                speechTexts.Title.Title,
                presenter.speechTexts.Title),
            GoToPage: presenter.getSpeechTextProperty(
                speechTexts.GoToPage.GoToPage,
                presenter.speechTexts.GoToPage),
            ShowNextPages: presenter.getSpeechTextProperty(
                speechTexts.ShowNextPages.ShowNextPages,
                presenter.speechTexts.ShowNextPages),
            NextPage: presenter.getSpeechTextProperty(
                speechTexts.NextPage.NextPage,
                presenter.speechTexts.NextPage)
        };
    };

    presenter.getSpeechTextProperty = function (rawValue, defaultValue) {
        if (rawValue === undefined || rawValue === null) {
            return defaultValue;
        }

        var value = rawValue.trim();
        if (value === '') {
            return defaultValue;
        }

        return value;
    };

    presenter.runLogic = function (view, model) {
    	presenter.$view = $(view);
        presenter.$wrapper = presenter.$view.find('.assessments-navigation-bar-wrapper');

        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);

        if (!validatedModel.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[validatedModel.errorCode], validatedModel.errorData);
            deleteCommands();
            return;
        }

        presenter.setSpeechTexts(model['speechTexts']);
        presenter.configuration = validatedModel;
        DOMOperationsUtils.setReducedSize(presenter.$view, presenter.$wrapper);

        presenter.calculateObjectsSizes();

        presenter.initializeAddon();

        if (presenter.isPreview) {
            if (presenter.navigationManager.buttons.length &gt; 0) {
                presenter.navigationManager.buttons[0].setAsCurrent();
            }
        } else {
            presenter.navigationManager.moveToCurrentPage();
        }

        presenter.buildKeyboardController();
        presenter.addHidingPageListListener();
        presenter.addEnabledPageListClass();
        presenter.handleSettingCurrentSectionClass();
    };

    function removeMockupDOM () {
        presenter.$wrapper.find(".navigation-buttons-first").remove();
        presenter.$wrapper.find(".navigation-buttons-last").remove();
        presenter.$wrapper.find(".sections").remove();
    }

    presenter.initializeAddon = function () {
        removeMockupDOM();

        presenter.sections = new presenter.Sections(presenter.configuration.sections);
        presenter.navigationManager = new presenter.NavigationManager();

        presenter.navigationManager.setSections();
    };

    function getVisibleSectionsNumber() {
        var hiddenSections = 0;
        for (var i = 0; i &lt; presenter.configuration.sections.length; i++) {
            if (presenter.configuration.sections[i].sectionName.trim().length == 0) hiddenSections += 1;
        }
        return presenter.configuration.sections.length - hiddenSections;
    }

    function calculateMaxNumberOfButtons () {
        var numberOfButtons;
        var numberOfElements = presenter.configuration.enableDropdownPagesList ? getVisibleSectionsNumber() : presenter.configuration.numberOfPages;
        if (presenter.configuration.userButtonsNumber) {
            numberOfButtons = presenter.configuration.maxElementCount &gt; (presenter.configuration.userButtonsNumber + 2) ?
                (presenter.configuration.userButtonsNumber + 2) : presenter.configuration.maxElementCount;

            if (numberOfButtons &gt; numberOfElements + 2) {
                numberOfButtons = (numberOfElements + 2);
            }
        } else {
            numberOfButtons = presenter.configuration.maxElementCount &gt; (numberOfElements + 2) ?
                (numberOfElements + 2) : presenter.configuration.maxElementCount;
        }
        presenter.configuration.numberOfButtons = numberOfButtons;
        presenter.configuration.navigationLeftIndex = 0;
        presenter.configuration.navigationRightIndex = numberOfButtons - 1;
    }

    function calculateButtonsSize(elementDistances) {
        var elementWidth;
        if (presenter.configuration.userButtonsWidth) {
            elementWidth = presenter.configuration.userButtonsWidth;
        } else {
            elementWidth = parseInt(presenter.$wrapper.width() / presenter.configuration.numberOfButtons - elementDistances.horizontal, 10);
        }

        var horizontalGap = presenter.$wrapper.width() - (elementWidth + elementDistances.horizontal) * presenter.configuration.maxElementCount;

        presenter.configuration.sizes = {
            elementWidth: elementWidth,
            horizontalGap: horizontalGap
        };
    }

    presenter.calculateObjectsSizes =   function() {
        var $element = presenter.$wrapper.find("." + presenter.CSS_CLASSES.PREVIOUS);

        var elementDimensions = DOMOperationsUtils.getOuterDimensions($element);
        var elementDistances = DOMOperationsUtils.calculateOuterDistances(elementDimensions);


        var elementBaseWidth;

        if (presenter.configuration.userButtonsWidth) {
            elementBaseWidth = presenter.configuration.userButtonsWidth;
        } else {
            elementBaseWidth = parseInt($element.width(), 10) + elementDistances.horizontal;
        }
        presenter.configuration.maxElementCount = parseInt((presenter.$wrapper.width()) / elementBaseWidth, 10) - 4;

        calculateMaxNumberOfButtons();
        calculateButtonsSize(elementDistances);
    };

    presenter.calculateNumberOfPages = function (sections) {
        return sections.reduce(function (result, section) {
            return result + section.pages.length;
        }, 0);
    };

    presenter.calculateNumberOfStaticPages = function (sections) {
        var staticSections = [];
        for (var i = 0; i &lt; sections.length; i++) {
            if (sections[i].staticPosition.length &gt; 0) {
                staticSections.push(sections[i]);
            }
        }
        return presenter.calculateNumberOfPages(staticSections);
    }

    presenter.validateModel = function (model) {
        var validatedSections = presenter.validateSections(model["Sections"]);

        if (!validatedSections.isValid) {
            return validatedSections;
        }

        var numberOfPages = presenter.calculateNumberOfPages(validatedSections.sections);
        var numberOfStaticPages = presenter.calculateNumberOfStaticPages(validatedSections.sections);

        var validateButtonsNumber = presenter.parseButtonsNumber(model["userButtonsNumber"], numberOfPages);
        if (!validateButtonsNumber.isValid) {
            return validateButtonsNumber;
        }

        var validateButtonsWidth = presenter.parseButtonsWidth(model["userButtonsWidth"]);
        if (!validateButtonsNumber.isValid) {
            return validateButtonsNumber;
        }

        return {
            isValid: true,
            addonID: model["ID"],
            sections: validatedSections.sections,
            addClassAreAllAttempted: ModelValidationUtils.validateBoolean(model["addClassAreAllAttempted"]),
            defaultOrder: ModelValidationUtils.validateBoolean(model["defaultOrder"]),
            userButtonsNumber: validateButtonsNumber.value,
            userButtonsWidth: validateButtonsWidth.value,
            numberOfPages: numberOfPages,
            numberOfStaticPages: numberOfStaticPages,
            useDynamicPagination: ModelValidationUtils.validateBoolean(model["useDynamicPagination"]),
            enableDropdownPagesList: ModelValidationUtils.validateBoolean(model["enableDropdownPagesList"]),
            enableRedirectToPage: ModelValidationUtils.validateBoolean(model["enableRedirectToPage"]),
        };
    };

    presenter.parseButtonsNumber = function (value, numberOfPages) {
        var buttonsNumber = presenter.parseNumericProperty(value, "S_06", "S_07");

        if (!buttonsNumber.isValid) {
            return buttonsNumber;
        }

        if (buttonsNumber.value &amp;&amp; buttonsNumber.value &gt; numberOfPages) {
            return getErrorObject("S_08");
        }

        return buttonsNumber;
    };

    presenter.parseButtonsWidth = function (value) {
        return presenter.parseNumericProperty(value, "S_09", "S_10");
    };

    presenter.parseNumericProperty = function (value, belowZeroError, nanError) {
        var trimmedValue = value.trim();
        if (ModelValidationUtils.isStringEmpty(trimmedValue)) {
            return {
                isValid: true,
                value: undefined
            };
        }

        var parsedValue = Number(trimmedValue);

        if (isNaN(parsedValue)) {
            return getErrorObject(nanError);
        }

        if (parsedValue &lt;= 0) {
            return getErrorObject(belowZeroError);
        }

        return {
            isValid: true,
            value: parseInt(parsedValue, 10)
        };
    };

    function isNotValid (element) {
        return element.isValid === false;
    }

    function isSectionPagesIntersecting (pagesA, pagesB) {
        return pagesA.some(function (element) {
            return this.indexOf(element) != -1;
        }, pagesB);
    }


    function validateSectionsIntersecting (sections) {
        var firstSection;
        var secondSection;
        var result = sections.every(function (section, sectionIndex) {
            for (var i = 0; i &lt; this.length; i++) {
                if (sectionIndex == i) {
                    continue;
                }

                if (isSectionPagesIntersecting(section.pages, this[i].pages)) {
                    firstSection = sectionIndex;
                    secondSection = i;
                    return false;
                }
            }

            return true;
        }, sections);

        return {
            isValid: result,
            firstSection: (firstSection + 1),
            secondSection: (secondSection + 1)
        };
    }

    presenter.validateSections = function (sections) {
        sections = sections.trim();
        if (ModelValidationUtils.isStringEmpty(sections)) {
            return getErrorObject("S_00");
        }

        var parsedSections = Helpers.splitLines(sections).map(getTrimmedStringElement).map(parseSection);
        var notValidSections = parsedSections.filter(isNotValid);

        if (notValidSections.length &gt; 0) {
            return notValidSections[0];
        }

        var validatedSections = validateSectionsIntersecting(parsedSections);

        if (!validatedSections.isValid) {
            return getErrorObject("S_05", {
                section_1: validatedSections.firstSection,
                section_2: validatedSections.secondSection
            });
        }

        parsedSections.map(function (element) {
            delete element.isValid;
            return element;
        });

        return {
            isValid: true,
            sections: parsedSections
        };
    };

    function getTrimmedStringElement(element) {
        return element.trim();
    }

    function parseDescriptions(descriptions, expectedLength, sectionIndex) {
        if (ModelValidationUtils.isStringEmpty(descriptions)) {
            return getErrorObject("S_03", {section: sectionIndex});
        }

        var parsedDescriptions = descriptions.split(",").map(getTrimmedStringElement);

        if (parsedDescriptions.length &gt; expectedLength) {
            return getErrorObject("S_04", {section: sectionIndex});
        }

        if (parsedDescriptions.length &lt; expectedLength) {
            return getErrorObject("S_03", {section: sectionIndex});
        }

        return {
            isValid: true,
            descriptions: parsedDescriptions
        };
    }

    presenter.parseSectionButtonsCssClassNames = function (cssClasses, sectionIndex) {
        if (ModelValidationUtils.isStringEmpty(cssClasses)) {
            return getErrorObject("S_11", {section: sectionIndex});
        }

        var parsedCssClasses = cssClasses.split(",").map(getTrimmedStringElement);
        var cssClassesWithoutDuplication = removeDuplicatedCssClassNames(parsedCssClasses);

        for (var i = 0; i &lt; cssClassesWithoutDuplication.length; i++) {
            if (ModelValidationUtils.isStringEmpty(cssClassesWithoutDuplication[i])) {
                return getErrorObject("S_11", {section: sectionIndex});
            }

            if (!isValidClassName(cssClassesWithoutDuplication[i])) {
                return getErrorObject("S_12", {section: sectionIndex});
            }
        }

        return {
            isValid: true,
            cssClasses: cssClassesWithoutDuplication
        };
    }

    function removeDuplicatedCssClassNames(cssClassNames) {
        return [...new Set(cssClassNames)];
    }

    function isValidClassName(className) {
        return /^[a-z_-][a-z\d_-]*$/i.test(className);
    }

    function changeToStringOneBigger (element) {
        return ((element + 1) + "");
    }

    function parseSection(section, sectionIndex) {
        section = section.split(";").map(getTrimmedStringElement);
        var len = section.length;
        var sectionName = "";
        var descriptions = [];
        var sectionButtonsCssClassNames = {cssClasses: []};
        var staticPosition = '';

        var pages = presenter.parsePagesFromRange(section[0], (sectionIndex + 1));

        if (!pages.isValid) {
            return pages;
        }

        if (len &gt; 1) {
            sectionName = getTrimmedStringElement(section[1]);
        }

        if (len &gt; 2) {
            descriptions = parseDescriptions(section[2], pages.pages.length, (sectionIndex + 1));
            if (!descriptions.isValid) {
                return descriptions;
            }
        } else {
            descriptions = {
                descriptions: pages.pages.map(changeToStringOneBigger)
            };
        }

        if (len &gt; 3) {
            if (len &lt;= 4 || getTrimmedStringElement(section[3]).length != 0) {
                sectionButtonsCssClassNames = presenter.parseSectionButtonsCssClassNames(section[3], (sectionIndex + 1));
                if (!sectionButtonsCssClassNames.isValid) {
                    return sectionButtonsCssClassNames;
                }
            }
        }

        if (len &gt; 4) {
            var rawPosition = getTrimmedStringElement(section[4]).toLowerCase();
            if (rawPosition == 'left') staticPosition = 'left';
            if (rawPosition == 'right') staticPosition = 'right';
            if (rawPosition.length &gt; 0 &amp;&amp; staticPosition.length == 0) {
                return getErrorObject("S_13", {section: sectionIndex});
            }
        }

        return {
            isValid: true,
            pages: pages.pages,
            sectionName: sectionName,
            pagesDescriptions: descriptions.descriptions,
            sectionButtonsCssClassNames: sectionButtonsCssClassNames.cssClasses,
            staticPosition: staticPosition
        }
    }

    function parseDashRange (section) {
        var result = [];
        var ranges = section.split("-");

        var min = Math.min(ranges[0], ranges[1]);
        var max = Math.max(ranges[0], ranges[1]);

        for (; max &gt;= min; min++) {
            result.push((min - 1));
        }

        return result;
    }

    function changeToIndexesZeroBased(element) {
        return (element - 1);
    }

    function parseCommaSeparatedRange (section) {
        return section.split(",").map(getTrimmedStringElement).map(Number).map(changeToIndexesZeroBased);
    }

    function getArrayOfPagesFromSection (section) {
        // 1-4 -&gt; true,
        // asdfa-123980fda -&gt; false,
        // 1, 2, 3, 4 -&gt; false
        var dashRangeTest = /^\d+-\d+$/;

        if (dashRangeTest.test(section)) {
            return parseDashRange(section);
        } else {
            return parseCommaSeparatedRange(section);
        }
    }

    presenter.isFloat = function (number) {
        return number % 1 !== 0;
    };

    function isNegativeOrZero (number) {
        return number &lt; 0;
    }


    presenter.parsePagesFromRange = function (section, sectionIndex) {
        var pages = getArrayOfPagesFromSection(section);

        for (var i = 0; i &lt; pages.length; i++) {
            if (isNaN(pages[i])) {
                return getErrorObject("S_02", {section: sectionIndex});
            }

            if (presenter.isFloat(pages[i])) {
                return getErrorObject("S_01", {section: sectionIndex});
            }

            if (isNegativeOrZero(pages[i])) {
                return getErrorObject("S_01", {section: sectionIndex});
            }
        }

        var sortedPages = pages.sort(function (a,b) {
            return a - b;
        });

        return {
            isValid: true,
            pages: sortedPages
        };
    };

    presenter.getState = function(){
        if (presenter.sections.allPages == null
        || presenter.sections.allPages.length ==0
        || presenter.navigationManager.actualPages.length == 0) return "";
        var pages = presenter.sections.allPages.map(function (page) {
            return {
                page: page.page,
                description: page.description,
                sectionName: page.sectionName,
                sectionCssClass: page.sectionCssClass,
                buttonCssClassNames: page.buttonCssClassNames,
                isBookmarkOn: page.isBookmarkOn,
                staticPosition: page.staticPosition,
            };
        });

        var state = {
            pages: pages,
            attemptedPages: presenter.sections.attemptedPages,
            leftSideIndex: presenter.navigationManager.getLeftSideIndex(),
            leftSideValue: presenter.navigationManager.actualPages[0].page,
            nextPrevBtnWasClicked: presenter.navigationManager.nextPrevBtnWasClicked
        };
        return JSON.stringify(state);
    };

    function getRestorePagesObjectArray (pages) {
        var restoredPages = pages.map(function (page) {
            var restoredPage = new presenter.Page(
                page.page, page.description, page.sectionName,
                page.sectionCssClass, page.buttonCssClassNames,
                page.staticPosition
            );
            restoredPage.setBookmarkOn(page.isBookmarkOn);

            return restoredPage;
        });

        if (presenter.sections.allPages.length &gt; 0) {
            var pagesIndexesInSections = presenter.sections.allPages.map(function(page) {
                return page.page;
            });

            return restoredPages.filter(function (page) {
                return pagesIndexesInSections.indexOf(page.page) !== -1;
            });
        } else {
            // if all pages are empty, then just return given state
            return restoredPages;
        }
    }

    presenter.setState = function (state) {
        if (state === null || state === "" || state === undefined) {
            return;
        }
        if (presenter.randomizeLesson === false
        || presenter.assessmentUser == presenter.ASSESSMENT_USER_TYPES.TEACHER) {
            //Randomize lesson == false and assessmentUser == teacher override state
            return;
        }

        var parsedState = JSON.parse(state);
        var upgradedState = presenter.upgradeState(parsedState);
        const previousLeftSideIndex = parsedState.leftSideIndex;
        const previousLeftSideValue = parsedState.leftSideValue;
        const nextPrevBtnWasClicked = parsedState.nextPrevBtnWasClicked;

        var restoredPages = getRestorePagesObjectArray(upgradedState.pages);
        // This if fix on wrong state when filter of sections worked wrong
        presenter.sections.allPages = restoredPages.length === presenter.sections.allPages.length ? restoredPages : presenter.sections.allPages;
        presenter.navigationManager.calculateLeftOffset(previousLeftSideValue, nextPrevBtnWasClicked);
        presenter.navigationManager.restartLeftSideIndex();
        presenter.navigationManager.setLeftSideIndex(previousLeftSideIndex, previousLeftSideValue);
        presenter.navigationManager.setSections();
        presenter.navigationManager.moveToCurrentPage();
        presenter.handleSettingCurrentSectionClass();

        presenter.sections.attemptedPages = upgradedState.attemptedPages;
        presenter.navigationManager.markButtonsWithAttempted(presenter.sections.attemptedPages);

        if (presenter.keyboardControllerObject != null) {
            if (presenter.configuration.enableDropdownPagesList) {
                presenter.keyboardControllerObject.setElements(presenter.getElementsForTTS());
                presenter.markCurrentSection();
            } else {
                presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
                presenter.markCurrentObject();
            }
        }
    };

    presenter.markCurrentObject = function () {
        const keyboardElements = presenter.keyboardControllerObject.keyboardNavigationElements;

        for (var i = 0; i &lt; keyboardElements.length; i++) {
            if ($(keyboardElements[i]).hasClass(presenter.CSS_CLASSES.CURRENT_PAGE)) {
                presenter.keyboardControllerObject.keyboardNavigationCurrentElementIndex = i;
            }
        }
    }

    presenter.markCurrentSection = function () {
        const keyboardElements = presenter.keyboardControllerObject.keyboardNavigationElements;

        for (var i = 0; i &lt; keyboardElements.length; i++) {
            const parentElement = $(keyboardElements[i]).parent();
            if ((parentElement).hasClass(presenter.CSS_CLASSES.CURRENT_SECTION)) {
                presenter.keyboardControllerObject.keyboardNavigationCurrentElementIndex = i;
            }
        }
    }

    presenter.upgradeState = function (state) {
        var upgradedState = presenter.upgradeAttemptedPages(state);
        upgradedState = presenter.upgradePagesButtonCssClassNames(upgradedState);
        return presenter.upgradePagesStaticPosition(upgradedState);
    };

    presenter.upgradeAttemptedPages = function (state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        if(state.attemptedPages === undefined) {
            upgradedState["attemptedPages"] = [];
        }

        return upgradedState;
    };

    presenter.upgradePagesButtonCssClassNames = function (state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        if (upgradedState.pages) {
            upgradedState.pages.map(function (page) {
                if(page.buttonCssClassNames === undefined) {
                    page["buttonCssClassNames"] = [];
                }
            });
        }

        return upgradedState;
    };

    presenter.upgradePagesStaticPosition = function (state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        if (upgradedState.pages) {
            upgradedState.pages.map(function (page) {
                if(page.staticPosition === undefined) {
                    page["staticPosition"] = '';
                }
            });
        }

        return upgradedState;
    };

    function getPlayerIndex(lessonIndex) {
        var mapping = presenter.playerController.getPagesMapping();
        return mapping[lessonIndex];
    }

    function currentPageAreAllAttempted() {
        var playerIndex = getPlayerIndex(presenter.currentPageIndex);
        if(presenter.presentation.getPage(playerIndex).isReportable()){

            var modules = getAllModulesImplementingIsAttempted(playerIndex);

            if(areAllModulesAttempted(modules)){
                presenter.sections.addClassAllAttemptedToPage(presenter.currentPageIndex);
            }else{
                presenter.sections.removeClassAllAttemptedToPage(presenter.currentPageIndex);
            }
        }
    }

    function getAllModulesImplementingIsAttempted(page) {
        var ids = presenter.playerController.getPresentation().getPage(page).getModulesAsJS(),
            modules = [];

        for(var i = 0; i &lt; ids.length; i++){
            var currentModule = presenter.playerController.getModule(ids[i]);

            if (currentModule &amp;&amp; currentModule.isAttempted !== undefined) {
                modules.push(currentModule);
            }
        }

        return modules;
    }

    function areAllModulesAttempted(modules) {
        if(modules.length === 0) return false;

        return modules.every(module =&gt; module.isAttempted());
    }

    presenter.areAllModulesAttempted = function () {
        currentPageAreAllAttempted();
    };

    presenter.reset = function () {
        presenter.sections.removeClassAllAttemptedToPage(presenter.currentPageIndex);
    };

    presenter.onEventReceived = function(eventName, eventData) {
        switch (eventName) {
            case "PageLoaded":
                if (presenter.configuration.addClassAreAllAttempted) {
                    presenter.areAllModulesAttempted();
                }
                break;
            case "ValueChanged":
                presenter.handleValueChanged(eventData);
                break;
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
        }
    };

    presenter.handleValueChanged = function (eventData) {
        if (presenter.configuration.addClassAreAllAttempted &amp;&amp; !presenter.isShowAnswersActive) {
            presenter.areAllModulesAttempted();
        }
        if (eventData.item === "Lesson Reset") {
            presenter.navigationManager.removeBookmarksFromButtons();
            presenter.navigationManager.removeAttemptedFromButtons();
        }
    };

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
    };

    presenter.hideAnswers = function () {
        presenter.isShowAnswersActive = false;
    };

    function AssesmentsNavigationKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    AssesmentsNavigationKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    AssesmentsNavigationKeyboardController.prototype.constructor = AssesmentsNavigationKeyboardController;

    AssesmentsNavigationKeyboardController.prototype.selectAction = function () {
        var $element = this.getTarget(this.keyboardNavigationCurrentElement, true);
        $element[0].click();
        if ($element.hasClass(presenter.CSS_CLASSES.TURN_BACK) || $element.hasClass(presenter.CSS_CLASSES.TURN_FORWARD)) {
            AssesmentsNavigationKeyboardController.prototype.setNewElements.call(this);
        };
    };

    AssesmentsNavigationKeyboardController.prototype.setNewElements = function () {
          if (presenter.isTTS()) {
              KeyboardController.prototype.setElements.call(this, presenter.getElementsForTTS());
          } else {
              KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
          }
    };

    AssesmentsNavigationKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    AssesmentsNavigationKeyboardController.prototype.switchElement = function (move) {
        var new_position_index = this.keyboardNavigationCurrentElementIndex + move;
        if (new_position_index &lt; this.keyboardNavigationElementsLen &amp;&amp; new_position_index &gt;= 0) {
            KeyboardController.prototype.markCurrentElement.call(this, new_position_index);
        }
        this.readCurrentElement();

        if (!presenter.isPageListNavigationActive) {
            presenter.currentSelectedChapterIndex = this.keyboardNavigationCurrentElementIndex;
        }
    };

    AssesmentsNavigationKeyboardController.prototype.enter = function (event) {
        if (presenter.isTTS() &amp;&amp; presenter.isFirstEnter) {
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForTTS());
            presenter.isFirstEnter = false;
            presenter.currentSelectedChapterIndex = 0;
        }
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    AssesmentsNavigationKeyboardController.prototype.select = function (event) {
        const selectedSectionClassName = $('.keyboard_navigation_active_element').parent().get(0).classList[0];
        const isSectionSelected = $(this.keyboardNavigationCurrentElement).hasClass('section_name');
        const isExpendable = presenter.configuration.enableDropdownPagesList &amp;&amp; isSectionSelected;

        if (isExpendable &amp;&amp; presenter.shouldRedirectToPage(selectedSectionClassName)) {
            presenter.handleRedirectToPage(selectedSectionClassName);
        } else if (isExpendable) {
            presenter.isPageListNavigationActive = true;
            presenter.changePageListVisibility(selectedSectionClassName);
            KeyboardController.prototype.setElements.call(this, presenter.getPageListElementsForTTS());
            presenter.keyboardControllerObject.keyboardNavigationCurrentElementIndex = 0;
            KeyboardController.prototype.markCurrentElement.call(this, 0);
        } else {
            KeyboardController.prototype.select.call(this, event);
        }
    }

    presenter.getButtonElement = function (sectionClassName) {
        const $parentElement = $(`.${sectionClassName}`);
        const $buttonsContainer = $parentElement.find('.buttons');

        return $buttonsContainer.get(0).firstChild;
    }

    presenter.changePageListVisibilityStatus = function () {
        setTimeout(function() {
            presenter.isPageListNavigationActive = false;
        }, 1000);
    }

    AssesmentsNavigationKeyboardController.prototype.escape = function (event) {
        if (presenter.configuration.enableDropdownPagesList &amp;&amp; presenter.isPageListNavigationActive) {
            const selectedSectionClassName = $('.keyboard_navigation_active_element').parent().get(0).classList[0];
            presenter.changePageListVisibility(selectedSectionClassName);
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForTTS());
            presenter.changePageListVisibilityStatus();
            presenter.keyboardControllerObject.switchElement(presenter.currentSelectedChapterIndex);
        } else {
            KeyboardController.prototype.escape.call(this, event);
        }
    }

    AssesmentsNavigationKeyboardController.prototype.exitWCAGMode = function () {
        if (presenter.isPageListNavigationActive) {
            const selectedSectionClassName = $('.keyboard_navigation_active_element').parent().get(0).classList[0];
            presenter.changePageListVisibility(selectedSectionClassName);
        }
        presenter.isFirstEnter = true;
        KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.exitWCAGMode.call(this);
    };

    presenter.isDeactivationBlocked = function() {
        return presenter.isPageListNavigationActive;
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject = new AssesmentsNavigationKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.addHidingPageListListener = function () {
        const $header = $('.ic_header');
        const $body = $('.ic_page');

        $header.on('click', function () {
            presenter.hideVisiblePageList();
        })

        $body.on('click', function () {
            presenter.hideVisiblePageList();
        })
    }

    presenter.addEnabledPageListClass = function () {
        if (presenter.configuration.enableDropdownPagesList) {
            this.$view.addClass(presenter.CSS_CLASSES.ENABLED_PAGE_LIST);
        }
    }

    presenter.getElementsForTTS = function () {
        return this.$view.find(".element:visible, .section_name:visible").filter(":not(.inactive)");
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return this.$view.find(".element:visible").filter(":not(.inactive)");
    };

    presenter.getPageListElementsForTTS = function () {
        const childrenWrapper = $('.keyboard_navigation_active_element').parent().get(0).children[1];
        return $(childrenWrapper).find('.element');
    }

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event)
    };

    AssesmentsNavigationKeyboardController.prototype.readCurrentElement = function () {
        var $element = this.getTarget(this.keyboardNavigationCurrentElement, false);
        if ($element.hasClass(presenter.CSS_CLASSES.PREVIOUS)) {
            presenter.speak(presenter.speechTexts.PreviousPage);
        } else if ($element.hasClass(presenter.CSS_CLASSES.TURN_BACK)) {
            presenter.speak(presenter.speechTexts.ShowPreviousPages);
        } else if ($element.hasClass(presenter.CSS_CLASSES.SECTION_NAME)) {
            presenter.speak(getSpeechForSectionName($element));
        } else if ($element.hasClass(presenter.CSS_CLASSES.BUTTON)) {
            presenter.speak(getSpeechForGoToPage($element));
        } else if ($element.hasClass(presenter.CSS_CLASSES.TURN_FORWARD)) {
            presenter.speak(presenter.speechTexts.ShowNextPages);
        } else if ($element.hasClass(presenter.CSS_CLASSES.NEXT)) {
            presenter.speak(presenter.speechTexts.NextPage);
        }
    };

    getSpeechForSectionName = function (element) {
        return `${presenter.speechTexts.Title} ${element[0].innerText}`;
    };

    getSpeechForGoToPage = function (element) {
        let pageName = element.find("." + presenter.CSS_CLASSES.BUTTON_TEXT).val();
        return `${presenter.speechTexts.GoToPage} ${element[0].innerText}`;
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function Assessments_Navigation_Bar_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function Assessments_Navigation_Bar_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.isTTS = function () {
        return presenter.getTextToSpeechOrNull(presenter.playerController) &amp;&amp; presenter.isWCAGOn;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Audio" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="mp3" nameLabel="Audio_property_mp3" type="audio"/>
		<property name="ogg" nameLabel="Audio_property_ogg" type="audio"/>
		<property name="defaultControls" nameLabel="Audio_property_default_controls" type="boolean"/>
        <property name="useBrowserControls" nameLabel="Audio_property_use_browser_controls" type="boolean"/>
		<property name="displayTime" nameLabel="Audio_property_display_time" type="boolean"/>
		<property name="enableLoop" nameLabel="Audio_property_enable_loop" type="boolean"/>
		<property isLocalized="true" name="Narration" nameLabel="Audio_property_narration" type="narration"/>
		<property name="onEnd" nameLabel="Audio_property_on_end" type="event"/>
		<property name="forceLoadAudio" nameLabel="Audio_property_force_load_audio" type="boolean"/>
		<property name="enablePlaybackSpeedControls" nameLabel="Audio_property_enable_playback_speed_controls" type="boolean"/>
	</model>
<css>.wrapper-addon-audio audio {
    bottom: 0;
    position: absolute;
}

.wrapper-addon-audio {
    height: 40px;
    position: relative;
}

.audioplayer {
    background-color: #EBEFF0;
    border-radius: 20px;
    width: 340px;
    height: 40px;
    display: table;
    padding-right: 2px;
    position: relative;
}

.audioplayer.playback-speed-control-enabled {
    width: 390px;
}

.play-pause-btn {
    width: 38px;
    height: 40px;
    float:left;
    border: solid 1px #444;
    border-radius: 30px;
    background-position: center center;
    background-repeat: no-repeat;
    cursor: pointer;
}

.audio-play-btn {
    background-image: url('resources/audio-play-btn.png');
}

.audio-pause-btn {
    background-image: url('resources/audio-pause-btn.png');
}

.audio-stop-btn {
    width: 40px;
    height: 40px;
    background: url('resources/audio-stop-btn.png') no-repeat center center;
    float:left;
    cursor: pointer;
}

.audio-volume-btn {
    width: 40px;
    height: 40px;
    background: url('resources/audio-volume-btn.png') no-repeat center center;
    float:left;
    cursor: pointer;
}

.audio-volume0 {
    background-image: url('resources/volume-0.png');
}

.audio-volume1 {
    background-image: url('resources/volume-1.png');
}

.audio-volume2 {
    background-image: url('resources/volume-2.png');
}

.audio-volume3 {
    background-image: url('resources/volume-3.png');
}

.player-time {
    vertical-align: middle;
    display: table-cell;
    height: 40px;
    line-height: 16px;
    padding-right: 10px;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
}

.audio-playback-rate {
    vertical-align: middle;
    display: table-cell;
    height: 40px;
    width: 60px;
    line-height: 16px;
    padding-right: 10px;
    background-color:inherit;
}

.audio-playback-rate select {
    width: 100%;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
    border: 0px;
    background-color:inherit;
}

.audio-progress-bar {
    -ms-touch-action: none;
    touch-action: none;
    width: 100px;
    float:left;
    padding-left: 0px;
    margin-top: 15px;
    margin-left: 5px;
    height: 10px;
    background-color: #FFF;
    border-radius: 5px;
    position: relative;
}

.audio-slider-btn {
    width: 20px;
    height: 20px;
    background: url('resources/audio-slider-btn.png') no-repeat;
    float:left;
    margin-left: -10px;
    margin-top: -5px;
    position: absolute;
    cursor: pointer;
}

.audio-bar {
    background-color: #BDC2C7;
    float: left;
    height: 10px;
    margin-left: 0px;
    border-bottom-left-radius: 5px;
    border-top-left-radius: 5px;
}

.audio-volume-layer {
    width: 100px;
    height: 40px;
    position: absolute;
    right: 15px;
    cursor: pointer;
}

.volume-control-background {
    position: absolute;
    width: 100%;
    height: 50%;
    border-bottom: solid 1px #808080;
}

.audio-volume-control {
    position: absolute;
    width: 20px;
    height: 40px;
    background: url('resources/audio-slider-btn.png') no-repeat center;
    margin-left: -10px;
}
</css><view>&lt;div class="wrapper-addon-audio"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="wrapper-addon-audio"&gt;

&lt;/div&gt;
</preview><presenter>function AddonAudio_create(){
    var presenter = function(){};
    var AUDIO_FILES_MISSING = "This addon needs at least 1 audio file.";
    var mp3File;
    var oggFile;
    var eventBus;
    var currentTimeAlreadySent;
    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);
    var audioIsLoaded = false;
    var fetchedAudioData;
    var isReadyToReplay = true;
    var wasPlayed = false;

    presenter.playbackRate = 1.0;
    var playbackRateList = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];

    function deferredQueueDecoratorChecker() {
        if (!presenter.configuration.forceLoadAudio) {
            return true;
        }

        return audioIsLoaded;
    }

    presenter.audio = {
        readyState : 0
    };
    presenter.playerController = null;
    presenter.addonID = null;
    presenter.type = 'audio';

    presenter.mouseData = {};

    presenter.onEventReceived = function AddonAudio_onEventReceived (eventName, eventData) {
        if (eventData.value == 'dropdownClicked' &amp;&amp; !presenter.audio.playing &amp;&amp; !isTemporarilyPaused()) {
            presenter.audio.load();
        }
    };

    function isTemporarilyPaused() {
        return (presenter.audio.paused
            &amp;&amp; presenter.audio.readyState &gt; 2
            &amp;&amp; presenter.audio.currentTime &gt; 0
            &amp;&amp; !presenter.audio.ended
        );
    }

    presenter.setPlayerController = function AddonAudio_setPlayerController (controller) {
        presenter.playerController = controller;
    };

    presenter.setEventBus = function AddonAudio_setEventBus (eventBusWrapper) {
        eventBus = eventBusWrapper;
    };

    presenter.upgradeModel = function AddonAudio_upgradeModel (model) {
        var upgradedModel = presenter.upgradeEnableLoop(model);
        upgradedModel = presenter.upgradeForceLoadAudio(upgradedModel);
        upgradedModel = presenter.upgradeEnablePlaybackSpeedControls(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeForceLoadAudio = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["forceLoadAudio"]) {
            upgradedModel["forceLoadAudio"] = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeEnableLoop = function AddonAudio_upgradeEnableLoop (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["enableLoop"]) {
            upgradedModel["enableLoop"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeEnablePlaybackSpeedControls = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["enablePlaybackSpeedControls"]) {
            upgradedModel["enablePlaybackSpeedControls"] = "False";
        }

        return upgradedModel;
    };

    presenter.createTimeUpdateEventData = function AddonAudio_createTimeUpdateEventData (data) {
        return {
            source : presenter.addonID,
            item : '',
            value : '' + data.currentTime,
            score : ''
        };
    };

    presenter.createOnEndEventData = function AddonAudio_createOnEndEventData () {
        return {
            source : presenter.addonID,
            item : 'end',
            value : '',
            score : ''
        };
    };

    presenter.getAudioCurrentTime = function AddonAudio_getAudioCurrentTime () {
        return this.audio.currentTime;
    };

    function addonAudio_formatTime (seconds) {
        return StringUtils.timeFormat(seconds);
    }

    presenter.AddonAudio_onProgressedCallback = function () {
        deferredSyncQueue.resolve();
    };

    presenter.AddonAudio_onLoadedMetadataCallback = function () {
        var duration = parseInt(presenter.audio.duration, 10);
        duration = isNaN(duration) ? 0 : duration;
        if (presenter.configuration.displayTime) {
            AddonAudio_displayTimer(0, duration);
        }
        if (presenter.configuration.isHtmlPlayer){
            presenter.$playerTime.html('0:00 / ' + addonAudio_formatTime(duration))
        }


        deferredSyncQueue.resolve();
    };

    presenter.sendEventAndSetCurrentTimeAlreadySent = function AddonAudio_sendEventAndSetCurrentTimeAlreadySent (eventData, currentTime) {
        eventBus.sendEvent('ValueChanged', eventData);
        currentTimeAlreadySent = currentTime;
    };

    presenter.sendOnEndEvent = function AddonAudio_sendOnEndEvent () {
        var eventData = presenter.createOnEndEventData();
        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onTimeUpdateSendEventCallback = function AddonAudio_onTimeUpdateSendEventCallback () {
        var currentTime = addonAudio_formatTime(presenter.getAudioCurrentTime());
        if (currentTime !== currentTimeAlreadySent) { // to prevent duplicated value
            var eventData = presenter.createTimeUpdateEventData({'currentTime' : currentTime});
            presenter.sendEventAndSetCurrentTimeAlreadySent(eventData, currentTime);
        }
    };

    function AddonAudio_onTimeUpdateCallback() {
        var bar_width, duration = presenter.audio.duration;
        duration = isNaN(duration) ? 0 : duration;
        var currentTime = presenter.audio.currentTime;
        if (presenter.configuration.displayTime) {
            AddonAudio_displayTimer(currentTime, duration);
        }
        if (presenter.configuration.isHtmlPlayer){
            presenter.$playerTime.html(addonAudio_formatTime(currentTime) + ' / ' + addonAudio_formatTime(duration));
            bar_width = presenter.$progressWrapper.width() * currentTime / duration;
            presenter.$progressBar.width(Math.round(bar_width));
            presenter.$progressSlider.css('left', Math.round(bar_width));
        }
    }

    function AddonAudio_change_volume_class(volume_class) {
        if (presenter.$volumeBtn.hasClass(volume_class)) {
            return;
        }
        for (var i=0; i&lt;=3; i++) {
            if (presenter.$volumeBtn.hasClass('audio-volume'+i)) {
                presenter.$volumeBtn.removeClass('audio-volume'+i);
            }
        }
        presenter.$volumeBtn.addClass(volume_class);
    }

    function AddonAudio_onVolumeChanged() {
        if (presenter.configuration.isHtmlPlayer){
            var volume = presenter.audio.volume,
                volume_class = '';
            presenter.$volumeControl.css('left', volume * presenter.$volumeLayer.width());
            if (volume &lt; 0.1) {
                volume_class = 'audio-volume0';
                if (volume &gt; 0) {
                    presenter.audio.volume = 0;
                }
            } else if (volume &lt; 0.4) {
                volume_class = 'audio-volume1';
            } else if (volume &lt; 0.7) {
                volume_class = 'audio-volume2';
            } else {
                volume_class = 'audio-volume3';
            }
            AddonAudio_change_volume_class(volume_class);
        }
    }

    function AddonAudio_playPauseCallback () {
        if (presenter.$playPauseBtn.hasClass('audio-pause-btn')) {
            presenter.pause();
        }
        else {
            presenter.play();
        }
    }

    function AddonAudio_displayTimer(current, duration) {
        presenter.$view.find('#currentTime').html(addonAudio_formatTime(current) + ' / ');
        presenter.$view.find('#durationTime').html(addonAudio_formatTime(duration));
    }

    function AddonAudio_progressMouseDownCallback(event) {
        if ($(event.target).hasClass('audio-slider-btn')) {
            presenter.mouseData.oldPosition = event.pageX;
            presenter.mouseData.isMouseDragged = true;
            presenter.mouseData.playedBeforeDragging = !presenter.audio.paused;
            if (!presenter.audio.paused) {
                presenter.pause();
            }
        }
    }

    function AddonAudio_progressMouseUpCallback() {
        if (presenter.mouseData.isMouseDragged) {
            var duration = presenter.audio.duration;
            duration = isNaN(duration) ? 0 : duration;
            presenter.audio.currentTime = duration * presenter.$progressBar.width() / presenter.$progressWrapper.width();
            presenter.mouseData.isMouseDragged = false;
            presenter.mouseData.oldPosition = 0;
            if (presenter.mouseData.playedBeforeDragging) {
                presenter.play();
            }
        }
    }

    function AddonAudio_progressMouseMoveCallback(event) {
        if (presenter.mouseData.isMouseDragged){
            var relativeDistance = event.pageX - presenter.mouseData.oldPosition,
                bar_width = 0,
                oldWidth = presenter.$progressBar.width();
            if (oldWidth + relativeDistance &lt; presenter.$progressWrapper.width()) {
                bar_width = oldWidth + relativeDistance;
            }
            else {
                bar_width = presenter.$progressWrapper.width();
            }
            presenter.$progressBar.width(bar_width);
            presenter.$progressSlider.css('left',Math.round(bar_width));
            presenter.mouseData.oldPosition = event.pageX;
        }
    }

    function addonAudio_isMoreThanOneFingerGesture(event) {
        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];
        if (event.hasOwnProperty('touches'))
            touchPoints = event.touches;
        return touchPoints.length&gt; 1;
    }

    function AddonAudio_progressTouchStartCallback(event) {
        var touch, touchPoints;
        if (addonAudio_isMoreThanOneFingerGesture(event)) return;

        touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0];
        else
            touch = touchPoints[0];
        AddonAudio_progressMouseDownCallback(touch);
    }


    function AddonAudio_progressTouchEndCallback() {
        AddonAudio_progressMouseUpCallback();
    }


    function AddonAudio_progressTouchMoveCallback(event) {
        if (addonAudio_isMoreThanOneFingerGesture(event)) return;

        var touch;
        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0];
        else
            touch = touchPoints[0];
        AddonAudio_progressMouseMoveCallback(touch);
    }

    function AddonAudio_attachProgressListeners() {
        presenter.isMobileDevice = MobileUtils.isMobileUserAgent(navigator.userAgent) || MobileUtils.isEventSupported('touchend');
        if (MobileUtils.isWindowsMobile(window.navigator)) {
            presenter.$progressWrapper[0].addEventListener("MSPointerDown", AddonAudio_progressTouchStartCallback, false);
            presenter.$progressWrapper[0].addEventListener("MSPointerUp", AddonAudio_progressTouchEndCallback, false);
            presenter.$progressWrapper[0].addEventListener("MSPointerMove", AddonAudio_progressTouchMoveCallback, false);
        }
        else if (presenter.isMobileDevice) {
            presenter.$progressWrapper[0].ontouchstart=AddonAudio_progressTouchStartCallback;
            presenter.$customPlayer[0].ontouchend=AddonAudio_progressTouchEndCallback;
            presenter.$progressWrapper[0].ontouchmove=AddonAudio_progressTouchMoveCallback;
        }
        else {
            presenter.$progressWrapper.on('mousedown', AddonAudio_progressMouseDownCallback);
            presenter.$progressWrapper.on('mouseup', AddonAudio_progressMouseUpCallback);
            presenter.$progressWrapper.on('mousemove', AddonAudio_progressMouseMoveCallback);
        }
    }

    function AddonAudio_toogleVolumeLayer(){
        AddonAudio_onVolumeChanged();
        presenter.$volumeLayer.toggle();
        presenter.$playerTime.toggle();
    }

    function AddonAudio_createHtmlPlayer() {
        presenter.$customPlayer = $('&lt;div&gt;').
            addClass('audioplayer');
        if (presenter.configuration.enablePlaybackSpeedControls) {
            presenter.$customPlayer.addClass('playback-speed-control-enabled');
        }

        presenter.$playPauseBtn = $('&lt;div&gt;').
            addClass('play-pause-btn').
            addClass('audio-play-btn').
            on('click', AddonAudio_playPauseCallback);

        presenter.$customPlayer.append(presenter.$playPauseBtn);

        presenter.$stopBtn = $('&lt;div&gt;').
            addClass('audio-stop-btn').
            on('click', presenter.stop);

        presenter.$customPlayer.append(presenter.$stopBtn);

        presenter.$progressWrapper = $('&lt;div&gt;').
            addClass('audio-progress-bar');

        presenter.$progressBar = $('&lt;div&gt;').
            addClass('audio-bar');

        presenter.$progressSlider = $('&lt;div&gt;').
            addClass('audio-slider-btn');

        AddonAudio_attachProgressListeners();

        presenter.$progressWrapper.
            append(presenter.$progressBar).
            append(presenter.$progressSlider);

        presenter.$customPlayer.append(presenter.$progressWrapper);

        if (!MobileUtils.isSafariMobile(navigator.userAgent)) {
            presenter.$volumeBtn = $('&lt;div&gt;').
                addClass('audio-volume-btn').
                on('click', AddonAudio_toogleVolumeLayer);

            presenter.$customPlayer.append(presenter.$volumeBtn);

            presenter.$volumeControlBackground = $('&lt;div&gt;').addClass('volume-control-background');
            presenter.$volumeControl = $('&lt;div&gt;').addClass('audio-volume-control');

            presenter.$volumeLayer = $('&lt;div&gt;').
                addClass('audio-volume-layer').
                append(presenter.$volumeControlBackground).
                append(presenter.$volumeControl).
                on('click', function (e) {
                presenter.audio.volume = e.offsetX / $(this).width();
            });

            presenter.$volumeLayer.hide();

            presenter.$customPlayer.append(presenter.$volumeLayer);
        }

        presenter.$playbackRateControls = null;
        if (presenter.configuration.enablePlaybackSpeedControls) {
            presenter.$playbackRateControls = $('&lt;div&gt;');
            presenter.$playbackRateControls.addClass('audio-playback-rate');
            presenter.$playbackRateControls.append(createPlaybackRateSelectElement());
            displayPlaybackRate();
            presenter.$customPlayer.append(presenter.$playbackRateControls);
        }

        presenter.$playerTime = $('&lt;div&gt;').
            addClass('player-time').
            text('00:00 / --:--');
        presenter.$customPlayer.append(presenter.$playerTime);

        presenter.$customPlayer.on('click mousedown mouseup', function(event){
            event.stopPropagation();
        });
        presenter.$audioWrapper.append(presenter.$customPlayer);
    }

    function createPlaybackRateSelectElement () {
        var $select = $('&lt;select&gt;');
        for (var i = 0; i &lt; playbackRateList.length; i++) {
            var $option = $('&lt;option&gt;');
            $option.text(playbackRateList[i]);
            $option.attr('value', playbackRateList[i]);
            if (playbackRateList[i] == 1) {
                $option.attr('selected', 'selected');
            }
            $select.append($option);
        }

        $select.on('change', function(){
            presenter.setPlaybackRate($select.val());
        })

        return $select;

    }

    function displayPlaybackRate () {
        if (presenter.$playbackRateControls != null) {
            var $select = presenter.$playbackRateControls.find('select');
            if ($select.val() == presenter.playbackRate) {
                if (playbackRateList.indexOf(presenter.playbackRate) != -1) {
                    $select.find('.custom-option').remove();
                }
                return;
            }
            $select.find('.custom-option').remove();
            if (playbackRateList.indexOf(presenter.playbackRate) != -1) {
                $select.val(presenter.playbackRate);
            } else {
                var $customOption = $('&lt;option&gt;');
                $customOption.text(presenter.playbackRate);
                $customOption.attr('value', presenter.playbackRate);
                $customOption.addClass('custom-option');
                $select.append($customOption);
                $select.val(presenter.playbackRate);

            }
        }
    }

    function AddonAudio_createView(view, model, isPreview){
        presenter.$audioWrapper = presenter.$view.find(".wrapper-addon-audio");

        mp3File = model.mp3;
        oggFile = model.ogg;

        if (!oggFile &amp;&amp; !mp3File) {
            $(view).html(AUDIO_FILES_MISSING);
        }

        var audio = document.createElement("audio");

        if (presenter.configuration.defaultControls){
            $(audio).attr("preload", "auto");
            if (presenter.configuration.isHtmlPlayer){
                AddonAudio_createHtmlPlayer();
            }
            else {
                $(audio).attr("controls", "controls");
            }

        }

        var currentTime = document.createElement("span");
        var durationTime = document.createElement("span");
        $(currentTime).attr("id", "currentTime");
        $(durationTime).attr("id", "durationTime");

        presenter.$audioWrapper.append(audio);
        if (presenter.configuration.displayTime) {
            presenter.$audioWrapper.append(currentTime).append(durationTime);
        }
        if (!isPreview) {
            audio.addEventListener('timeupdate', presenter.onTimeUpdateSendEventCallback, false);
        }

        Object.defineProperty(audio, 'playing', {
            get: function() {
                return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
            }
        });

        presenter.audio = audio;
    }

    presenter.sendOnPLayingEvent = function () {
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'playing',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.sendOnPauseEvent = function () {
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'pause',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.sendNotStartedEvent = function () {
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'not-started',
        };

        eventBus.sendEvent('PreDestroyed', eventData);
    };

    presenter.sendReplayedInLoopEvent = function () {
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'replayed-in-loop',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    function AddonAudio_onAudioPlaying () {
        presenter._setPlayed(true);
        presenter.sendOnPLayingEvent();
    }

    function AddonAudio_onAudioPause () {
        presenter.sendOnPauseEvent();
    }

    function AddonAudio_attachEventListeners(audio) {
        audio.addEventListener('loadeddata', presenter.AddonAudio_onLoadedMetadataCallback, false);
        audio.addEventListener('timeupdate', AddonAudio_onTimeUpdateCallback, false);
        audio.addEventListener('volumechange', AddonAudio_onVolumeChanged, false);
        audio.addEventListener('ended', AddonAudio_onAudioEnded , false);
        audio.addEventListener('click', AddonAudio_onAudioClick, false);
        audio.addEventListener('playing', AddonAudio_onAudioPlaying, false);
        audio.addEventListener('pause', AddonAudio_onAudioPause, false);

        if (isMobileIOS()) {
            AddonAudio_attachEventListenersForIOS(audio);
        }
    }

    function AddonAudio_attachEventListenersForIOS(audio) {
        if (presenter.configuration.forceLoadAudio) {
            audio.addEventListener('progress', presenter.AddonAudio_onProgressedCallback, { once: true });
        }
    }

    function AddonAudio_removeEventListeners(audio) {
        presenter.audio.removeEventListener('loadeddata', presenter.AddonAudio_onLoadedMetadataCallback, false);
        presenter.audio.removeEventListener('timeupdate', presenter.onTimeUpdateSendEventCallback, false);
        presenter.audio.removeEventListener('timeupdate', AddonAudio_onTimeUpdateCallback, false);
        presenter.audio.removeEventListener('volumechange', AddonAudio_onVolumeChanged, false);
        presenter.audio.removeEventListener('ended', AddonAudio_onAudioEnded , false);
        presenter.audio.removeEventListener('click', AddonAudio_onAudioClick, false);
        presenter.audio.removeEventListener('playing', AddonAudio_onAudioPlaying, false);
        presenter.audio.removeEventListener('pause', AddonAudio_onAudioPause, false);

        if (isMobileIOS()) {
            AddonAudio_removeEventListenersForIOS(audio);
        }
    }

    function AddonAudio_removeEventListenersForIOS(audio) {
        if (presenter.configuration.forceLoadAudio) {
            audio.removeEventListener('progress', presenter.AddonAudio_onProgressedCallback, { once: true });
        }
    }

    function isMobileIOS() {
        return window.MobileUtils.isSafariMobile(navigator.userAgent);
    }

    function AddonAudio_onAudioEnded () {
        if (presenter.configuration.enableLoop) {
            presenter.sendReplayedInLoopEvent();
            this.currentTime = 0;
            this.play();
        } else {
            presenter.executeOnEndEvent();
            presenter.sendOnEndEvent();
            presenter.stop();
        }
    }

    function AddonAudio_onAudioClick (e) {
        e.stopPropagation();
    }

    presenter.fetchAudioFromServer = function (src) {
        var req = new XMLHttpRequest();
        req.open('GET', src, true);
        req.responseType = 'blob';
        req.addEventListener("load", presenter.loadAudioDataFromRequest);

        req.send();
    };

    presenter.loadAudioDataFromRequest = function (event) {
        if (event.currentTarget.status == 200 || (isMobileIOS() &amp;&amp; event.currentTarget.status == 0)) {
            fetchedAudioData = event.currentTarget.response;
            presenter.audio.src = URL.createObjectURL(fetchedAudioData);
            audioIsLoaded = true;
        }
    };

    function AddonAudio_loadFiles(){
        var canPlayMp3 = false;
        var canPlayOgg = false;
        var audio = presenter.audio;

        AddonAudio_attachEventListeners(audio);

        if(audio.canPlayType) {
            canPlayMp3 = audio.canPlayType &amp;&amp; "" != audio.canPlayType('audio/mpeg');
            canPlayOgg = audio.canPlayType &amp;&amp; "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
            var audioSrc = "";

            if(canPlayMp3){
                audioSrc = mp3File;
            } else if (canPlayOgg) {
                audioSrc = oggFile;
            }

            if (presenter.configuration.forceLoadAudio) {
                presenter.fetchAudioFromServer(audioSrc);
            } else {
                $(audio).attr("src", audioSrc);
            }

        } else {
            $(audio).append("Your browser doesn't support audio.");
        }

        $(audio).load();
    }

    presenter.run = function AddonAudio_run (view, model){
        presenter.initialize(view, model, false);
        presenter.addonID = model.ID;
        eventBus.addEventListener('ValueChanged', this);
    };

    presenter.createPreview = function AddonAudio_createPreview (view, model){
        presenter.initialize(view, model, true);
    };

    presenter.initialize = function AddonAudio_initialize (view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.view = view;
        presenter.$view = $(view);
        presenter.configuration = presenter.validateModel(upgradedModel);

        AddonAudio_createView(view, upgradedModel, isPreview);
        
        if (!isPreview) {
        	AddonAudio_loadFiles();
            presenter.$view.bind('click', function (event) {
                event.stopPropagation();
            });
        }
    };

    presenter.onDestroy = function () {
        presenter.audio.pause();

        presenter.playerController = null;

        AddonAudio_removeEventListeners(presenter.audio);
        if (presenter.configuration.forceLoadAudio &amp;&amp; audioIsLoaded) {
            URL.revokeObjectURL(presenter.audio.src);
        }
        presenter.audio.setAttribute('src', '');
        presenter.audio.load();
        presenter.audio = null;

        if (presenter.$playPauseBtn) {
            presenter.$playPauseBtn.off();
            presenter.$playPauseBtn = null;
        }
        if (presenter.$stopBtn) {
            presenter.$stopBtn.off();
            presenter.$stopBtn = null;
        }
        if (presenter.$progressWrapper) {
            presenter.$progressWrapper[0].removeEventListener("MSPointerDown", AddonAudio_progressTouchStartCallback, false);
            presenter.$progressWrapper[0].removeEventListener("MSPointerUp", AddonAudio_progressTouchEndCallback, false);
            presenter.$progressWrapper[0].removeEventListener("MSPointerMove", AddonAudio_progressTouchMoveCallback, false);
            presenter.$progressWrapper[0].ontouchstart=null;
            presenter.$progressWrapper[0].ontouchmove=null;
            presenter.$progressWrapper.off();
            presenter.$progressWrapper = null;
        }
        if (presenter.$customPlayer) {
            presenter.$customPlayer.off();
            presenter.$customPlayer[0].ontouchend=null;
            presenter.$customPlayer = null;
        }
        if (presenter.$volumeBtn) {
            presenter.$volumeBtn.off();
            presenter.$volumeBtn = null;
        }
        if (presenter.$volumeLayer) {
            presenter.$volumeLayer.off();
            presenter.$volumeLayer = null;
        }

        presenter.$volumeControlBackground = null;
        presenter.$volumeControl = null;
        presenter.$audioWrapper = null;
        presenter.$playerTime = null;
        presenter.$progressBar = null;
        presenter.$progressSlider = null;

        presenter.mouseData = null;
        presenter.configuration = null;
        presenter.$view.unbind();
        presenter.$view = null;
        presenter.view = null;

        deferredSyncQueue = null;
    };

    presenter.validateModel = function AddonAudio_validateModel (model) {
        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]),
            defaultControls = ModelValidationUtils.validateBoolean(model.defaultControls),
            useBrowserControls = ModelValidationUtils.validateBoolean(model.useBrowserControls);

        return {
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            onEndEventCode: model.onEnd,
            enableLoop: ModelValidationUtils.validateBoolean(model.enableLoop),
            displayTime: ModelValidationUtils.validateBoolean(model.displayTime),
            defaultControls: defaultControls,
            useBrowserControls: useBrowserControls,
            isHtmlPlayer: defaultControls &amp;&amp; !useBrowserControls,
            addonID: model.ID,
            forceLoadAudio: ModelValidationUtils.validateBoolean(model.forceLoadAudio),
            narration: model.Narration,
            enablePlaybackSpeedControls: ModelValidationUtils.validateBoolean(model.enablePlaybackSpeedControls)
        };
    };

    presenter.executeCommand = function AddonAudio_executeCommand (name, params) {
        var commands = {
            'play': presenter.play,
            'stop': presenter.stop,
            'show': presenter.show,
            'hide': presenter.hide,
            'pause': presenter.pause,
            'getNarration': presenter.getNarration,
            'setPlaybackRate': presenter.setPlaybackRate
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function AddonAudio_setVisibility (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.play = deferredSyncQueue.decorate(function() {
        if (!presenter.audio) return;
        if (presenter.audio.src &amp;&amp; presenter.audio.paused) {
            if (!isReadyToReplay) {
                prepareToReplay();
            }
            presenter.audio.play();
            if (presenter.configuration.isHtmlPlayer) {
                presenter.$playPauseBtn.
                    removeClass('audio-play-btn').
                    addClass('audio-pause-btn');
            }
        }
    });

    presenter.pause = deferredSyncQueue.decorate(function AddonAudio_pause () {
        if (!presenter.audio) return;
        if(presenter.audio.readyState &gt; 0) {
            if (!presenter.audio.paused) {
                presenter.audio.pause();
            }
            if (presenter.configuration.isHtmlPlayer) {
                presenter.$playPauseBtn.
                    removeClass('audio-pause-btn').
                    addClass('audio-play-btn');
            }
        }
    });

    function prepareToReplay() {
        if (isMobileIOS()) {
            prepareToReplayOnMobileIOS()
        }
        isReadyToReplay = true;
    }

    function prepareToReplayOnMobileIOS() {
        if (presenter.configuration.forceLoadAudio) {
            URL.revokeObjectURL(presenter.audio.src);
            presenter.audio.src = URL.createObjectURL(fetchedAudioData);
        }
    }

    presenter.stop = deferredSyncQueue.decorate(function AddonAudio_stop () {
        if (!presenter.audio) return;
        if(presenter.audio.readyState &gt; 0) {
            isReadyToReplay = false;
            presenter.pause();
            presenter.audio.currentTime = 0;
        }
    });

    function increasePlaybackRate() {
        const playbackIndex = playbackRateList.indexOf(presenter.playbackRate);
        const nextPlaybackIndex = Math.min(playbackIndex+1, playbackRateList.length-1);
        presenter.setPlaybackRate(("" + playbackRateList[nextPlaybackIndex]));
    }

    function decreasePlaybackRate() {
        const playbackIndex = playbackRateList.indexOf(presenter.playbackRate);
        const nextPlaybackIndex = Math.max(playbackIndex-1, 0);
        presenter.setPlaybackRate(("" + playbackRateList[nextPlaybackIndex]));
    }

    presenter.setPlaybackRate = function (value) {
        if (!presenter.audio) return;
        if (isNaN(value)) return;
        var parsedValue = parseFloat(value);
        presenter.playbackRate = parsedValue;
        presenter.audio.playbackRate = parsedValue;
        displayPlaybackRate();
    };

    presenter.show = function AddonAudio_show () {
        this.setVisibility(true);
        this.configuration.isVisible = true;
    };

    presenter.hideAddon = function AddonAudio_hideAddon () {
        this.setVisibility(false);
        this.configuration.isVisible = false;
    };

    presenter.hide = function AddonAudio_hide () {
        this.stop();
        this.hideAddon();
    };

    presenter.reset = function AddonAudio_reset () {
        if (!presenter.audio) return;

        presenter.stop();

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        if (presenter.configuration.isVisible) {
            presenter.show();
        } else {
            presenter.hideAddon();
        }
    };

    presenter.getState = function AddonAudio_getState () {
        return JSON.stringify({
            isVisible : presenter.configuration.isVisible,
            playbackRate: presenter.playbackRate,
            wasPlayed: wasPlayed
        });
    };

    presenter.setState = function addonAudio_setState (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) {
            return false;
        }

        /*
        * The If statement resolves an issue on Mobile Safari, where presenter.stop call from setState would be resolved
        * right after the first presenter.play call. The problem occurs because the deferred queue is resolved
        * on loadedmetadata event and Mobile Safari only downloads the audio/video src file on the first interaction
        * with the tag, rather than when the page itself is loaded.
        * */
        if (audioIsLoaded) {
            presenter.stop();
        };

        var parsedJson = JSON.parse(stateString);
        if (parsedJson.isVisible) {
            this.show();
        } else {
            this.hideAddon();
        }
        if (parsedJson['playbackRate'] != undefined) {
            presenter.setPlaybackRate(parsedJson['playbackRate']);
        }
        if (parsedJson.wasPlayed !== undefined) {
            wasPlayed = parsedJson.wasPlayed;
        }

        return false;
    };

    presenter.executeOnEndEvent = function AddonAudio_executeOnEndEvent () {
        if (presenter.configuration.onEndEventCode) {
            presenter.playerController.getCommands().executeEventCode(presenter.configuration.onEndEventCode);
        }
    };

    presenter.getNarration = function AddonAudio_getNarration() {
        return presenter.configuration.narration;
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        //This method has been added to enable the addon's detection by the autofill option of TTS
    };

    function forward() {
        presenter.audio.currentTime += 5;
    }

    function backward() {
        presenter.audio.currentTime -= 5;
    }

    function playPause() {
        if (presenter.audio.paused) {
            presenter.play();
        } else {
            presenter.pause();
        }
    }

    function increaseVolume() {
         var volume = presenter.audio.volume;
        volume += 0.1;
        if (volume &gt; 1.0) volume = 1.0;
        presenter.audio.volume = volume;
    }

    function decreaseVolume() {
        var volume = presenter.audio.volume;
        volume -= 0.1;
        if (volume &lt; 0.0) volume = 0.0;
        presenter.audio.volume = volume;
    }

    presenter.keyboardController = function (keycode, isShift, event, keysDownCodes) {
        event.preventDefault();

        if (keysDownCodes === undefined) {
            keysDownCodes = [];
        }
        const letterSKeyCode = 83;
        switch (keycode) {
            case window.KeyboardControllerKeys.TAB:
                if (isShift) {
                    backward();
                } else {
                    forward();
                }
                break;
            case window.KeyboardControllerKeys.ENTER:
                if (isShift) {
                    presenter.stop();
                }
                break;
            case window.KeyboardControllerKeys.SPACE:
                playPause();
                break;
            case window.KeyboardControllerKeys.ARROW_UP:
                if (keysDownCodes.includes(letterSKeyCode)) {
                    increasePlaybackRate();
                } else {
                    increaseVolume();
                }
                break;
            case window.KeyboardControllerKeys.ARROW_DOWN:
                if (keysDownCodes.includes(letterSKeyCode)) {
                    decreasePlaybackRate();
                } else {
                    decreaseVolume();
                }
                break;
            case window.KeyboardControllerKeys.ARROW_LEFT:
                backward();
                break;
            case window.KeyboardControllerKeys.ARROW_RIGHT:
                forward();
                break;
            case window.KeyboardControllerKeys.ESCAPE:
                presenter.stop();
                break;
        }
    };

    presenter._setPlayed = function (_wasPlayed) {
        wasPlayed = _wasPlayed;
    };

    presenter.preDestroy = function() {
        if (!wasPlayed) {
            presenter.sendNotStartedEvent();
        }
    };

    return presenter;
}

AddonAudio_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="AudioPlaylist" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Items" nameLabel="AudioPlaylist_property_files" type="list">
			<property name="Name" nameLabel="AudioPlaylist_property_name" type="string"/>
			<property name="Mp3" nameLabel="AudioPlaylist_property_mp3" type="audio"/>
			<property name="Ogg" nameLabel="AudioPlaylist_property_ogg" type="audio"/>
		</property>
		<property name="Stop playing" nameLabel="AudioPlaylist_property_stop_playing" type="boolean"/>
		<property displayName="Enable audio speed controller" name="Enable audio speed controller" nameLabel="Enable_playback_speed_controls" type="boolean"/>
		<property name="langAttribute" nameLabel="AudioPlaylist_lang_attribute" type="string"/>
		<property name="speechTexts" nameLabel="AudioPlaylist_property_speech_texts" type="staticlist">

            <property name="play" nameLabel="AudioPlaylist_play" type="staticrow">
                <property name="play" nameLabel="AudioPlaylist_play" type="string"/>
            </property>

            <property name="pause" nameLabel="AudioPlaylist_pause" type="staticrow">
                <property name="pause" nameLabel="AudioPlaylist_pause" type="string"/>
            </property>

            <property name="prev" nameLabel="AudioPlaylist_prev" type="staticrow">
                <property name="prev" nameLabel="AudioPlaylist_prev" type="string"/>
            </property>

            <property name="next" nameLabel="AudioPlaylist_next" type="staticrow">
                <property name="next" nameLabel="AudioPlaylist_next" type="string"/>
            </property>

            <property name="audioSpeedController" nameLabel="AudioPlaylist_audioSpeedController" type="staticrow">
                <property name="audioSpeedController" nameLabel="AudioPlaylist_audioSpeedController" type="string"/>
            </property>

            <property name="volume" nameLabel="AudioPlaylist_volume" type="staticrow">
                <property name="volume" nameLabel="AudioPlaylist_volume" type="string"/>
            </property>

            <property name="timer" nameLabel="AudioPlaylist_timer" type="staticrow">
                <property name="timer" nameLabel="AudioPlaylist_timer" type="string"/>
            </property>

            <property name="audioItem" nameLabel="AudioPlaylist_audioItem" type="staticrow">
                <property name="audioItem" nameLabel="AudioPlaylist_audioItem" type="string"/>
            </property>
		</property>
	</model>
<css>.wrapper-addon-audio-playlist audio {
    bottom: 0;
    position: absolute;
}

.wrapper-addon-audio-playlist {
    height: 100%;
    background-color: white;
    border-radius: 10px;
    padding: 15px;
    box-sizing: border-box;
}

div.addon-audio-playlist-controls {
    padding: 10px 25px;
    position: relative;
    border: 1px solid black;
    border-radius: 30px;
    display: flex;
}

.audioplayer {
    background-color: #EBEFF0;
    border-radius: 20px;
    width: 340px;
    height: 40px;
    display: table;
    padding-right: 2px;
    position: relative;
}

button.audio-playlist-control-btn {
    width: 38px;
    height: 40px;
    background-position: center center;
    background-repeat: no-repeat;
    cursor: pointer;
    border: 0;
    background-color: white;
}

button.audio-playlist-next-btn {
    background-image: url('resources/audio-next-btn.png');
}

button.audio-playlist-prev-btn {
    background-image: url('resources/audio-prev-btn.png');
}

button.audio-playlist-play-pause-btn {
    border: solid 3px #444;
    border-radius: 30px;
}

button.audio-playlist-play-btn {
    background-image: url('resources/audio-play-btn.png');
}

button.audio-playlist-pause-btn {
    background-image: url('resources/audio-pause-btn.png');
}

.audio-playlist-volume-btn {
    background: url('resources/audio-volume-btn.png');
    margin-left: auto;
}

div.audio-playlist-slider {
    display: flex;
    flex-grow: 1;
    flex-direction: column;
}

.addon-audio-playlist-items-wrapper {
    height: calc(100% - 62px);
    padding: 0 25px;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: #444 #F4F5F6;
}

.addon-audio-playlist-items-wrapper::-webkit-scrollbar {
    width: 8px;
}
.addon-audio-playlist-items-wrapper::-webkit-scrollbar-thumb {
    background-color: #444;
}

.addon-audio-playlist-items-wrapper::-webkit-scrollbar-track {
    background: #F4F5F6;
}


div.addon-audio-playlist-items {
    padding: 10px;
}

div.addon-audio-playlist-item {
    display: flex;
    border-bottom: 1px solid lightgray;
    padding: 10px;
    background-color: white;
}

div.addon-audio-playlist-item-selected {
    background-color: #F4F5F6;
}

span.addon-audio-playlist-item--name {
    flex-grow: 1;
    padding: 0 5px;
}

button.addon-audio-playlist-item--button {
    width: 20px;
    height: 20px;
    background-position: center center;
    background-repeat: no-repeat;
    cursor: pointer;
    background-image: url('resources/audio-play-btn.png');
    border: 1px solid gray;
    border-radius: 15px;
    background-size: 10px;
    background-color: white;
}

button.addon-audio-playlist-item--button-playing {
    background-image: url('resources/audio-pause-btn.png');
}


div.audio-playlist-timer {
    font-size: 15px;
}

.audio-volume0 {
    background-image: url('resources/volume-0.png');
}

.audio-volume1 {
    background-image: url('resources/volume-1.png');
}

.audio-volume2 {
    background-image: url('resources/volume-2.png');
}

.audio-volume3 {
    background-image: url('resources/volume-3.png');
}

.player-time {
    vertical-align: middle;
    display: table-cell;
    height: 40px;
    line-height: 16px;
    padding-right: 10px;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
}

.audio-playlist-progress-bar {
    -ms-touch-action: none;
    touch-action: none;
    width: 100px;
    margin-left: 15px;
    background-color: #FFF;
    border-radius: 5px;
    flex-grow: 1;
    display: flex;
}

.audio-playlist-slider-btn {
    width: 20px;
    height: 20px;
    background: url('resources/audio-slider-btn.png') no-repeat;
    float: left;
    margin-left: -10px;
    margin-top: -5px;
    position: absolute;
    cursor: pointer;
}

div.audio-playlist-bar {
    background-color: #BDC2C7;
    float: left;
    height: 10px;
    margin-left: 0;
    position: relative;
    width: 100%;
    border-radius: 5px;
    border: 0;
}

div.audio-playlist-bar--fill {
    height: 10px;
    width: 0;
    background-color: #0A8DBD;
    border-radius: 5px;
}

.addon-audio-playlist-bar--ball {
    cursor: pointer;
    position: absolute;
    background: url("resources/audio-slider-btn.png");
    width: 14px;
    height: 14px;
    background-size: contain;
    margin-top: -2px;
    margin-left: -2px;
    top: 0;
    left: 0;
}

div.audio-playlist-max-time {
    text-align: center;
    vertical-align: middle;
    line-height: 40px;
    padding: 0 15px;
}

.addon-audio-playlist-volume-wrapper {
    width: auto;
    display: flex;
}

.addon-audio-playlist-volume-wrapper--expanded {
    width: 30%;
}

.addon-audio-playlist-volume-bar--hidden {
    display: none;
}

.addon-audio-playlist-volume-bar {
    margin: auto;
    background-color: #BDC2C7;
    height: 5px;
    margin-left: 0;
    position: relative;
    width: 100%;
    border-radius: 2px;
    border: 0;
}

.addon-audio-playlist-volume-bar-fill {
    position: absolute;
    cursor: pointer;
    left: 1px;
    top: 0;
    bottom: 0;
    height: 5px;
    background-color: #0A8DBD;
    border-radius: 2px;
    width: 100%;
}

.audio-volume-control {
    position: absolute;
    width: 20px;
    height: 40px;
    background: url('resources/audio-slider-btn.png') no-repeat center;
    margin-left: -10px;
}

.audio-speed-controller {
    float: left;
    margin-top: 10px;
    height: 30px;
    width: 60px;
    line-height: 16px;
    padding-right: 10px;
    background-color: inherit;
}

.audio-speed-controller select {
    width: 100%;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
    border: 0;
    background-color: inherit;
}
</css><view>&lt;div class="wrapper-addon-audio-playlist"&gt;
    &lt;div class="addon-audio-playlist-controls"&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-prev-btn"&gt;&lt;/button&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-play-pause-btn audio-playlist-play-btn"&gt;&lt;/button&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-next-btn"&gt;&lt;/button&gt;
        &lt;div class="audio-playlist-progress-bar"&gt;
            &lt;div class="audio-playlist-slider"&gt;
                &lt;div class="audio-playlist-timer"&gt;00:00&lt;/div&gt;
                &lt;div class="audio-playlist-bar"&gt;
                    &lt;div class="audio-playlist-bar--fill"&gt;&lt;/div&gt;
                    &lt;div class="addon-audio-playlist-bar--ball"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="audio-playlist-max-time"&gt;00:00&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="audio-speed-controller" style="display: none"&gt;&lt;/div&gt;
        &lt;div class="addon-audio-playlist-volume-wrapper"&gt;
            &lt;button class="audio-playlist-control-btn audio-playlist-volume-btn"&gt;&lt;/button&gt;
            &lt;div class="addon-audio-playlist-volume-bar addon-audio-playlist-volume-bar--hidden"&gt;
                &lt;div class="addon-audio-playlist-volume-bar-fill"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="addon-audio-playlist-items-wrapper"&gt;
        &lt;div class="addon-audio-playlist-items"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;audio&gt;&lt;/audio&gt;
&lt;/div&gt;
</view><preview>&lt;div class="wrapper-addon-audio-playlist"&gt;
    &lt;div class="addon-audio-playlist-controls"&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-prev-btn"&gt;&lt;/button&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-play-pause-btn audio-playlist-play-btn"&gt;&lt;/button&gt;
        &lt;button class="audio-playlist-control-btn audio-playlist-next-btn"&gt;&lt;/button&gt;
        &lt;div class="audio-playlist-progress-bar"&gt;
            &lt;div class="audio-playlist-slider"&gt;
                &lt;div class="audio-playlist-timer"&gt;00:00&lt;/div&gt;
                &lt;div class="audio-playlist-bar"&gt;
                    &lt;div class="audio-playlist-bar--fill" style="width: 50%"&gt;&lt;/div&gt;
                    &lt;div class="addon-audio-playlist-bar--ball"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="audio-playlist-max-time"&gt;00:00&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="audio-speed-controller" style="display: none"&gt;&lt;/div&gt;
        &lt;div class="addon-audio-playlist-volume-wrapper"&gt;
            &lt;button class="audio-playlist-control-btn audio-playlist-volume-btn"&gt;&lt;/button&gt;
            &lt;div class="addon-audio-playlist-volume-bar addon-audio-playlist-volume-bar--hidden"&gt;
                &lt;div class="addon-audio-playlist-volume-bar-fill"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="addon-audio-playlist-items-wrapper"&gt;
        &lt;div class="addon-audio-playlist-items"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonAudioPlaylist_create() {
    var presenter = function () {
    };

    var eventBus;

    presenter.ERROR_CODES = {
        'ID_STR01': "Value provided to text property is empty.",
        'ID_STR02': "Value provided to text property is not a valid string.",
        'Is Visible_UMF01': "Is Visible cannot be found.",
        'Stop playing_UMF01': "Stop playing could not be found",
        'items|name_STR01': "Value provided to name property of the item is empty."
    };

    presenter.DEFAULT_TTS_PHRASES = {
        PLAY: "Play button",
        PAUSE: "Pause button",
        PREVIOUS_AUDIO: "Previous audio",
        NEXT_AUDIO: "Next audio",
        AUDIO_SPEED_CONTROLLER: "Audio speed controller",
        VOLUME: "Volume level",
        TIMER: "Time",
        AUDIO_ITEM: "Audio item"
    };

    presenter.NAVIGATION_ELEMENT = {
        PLAY: "Play",
        PAUSE: "Pause",
        PREVIOUS_AUDIO: "PreviousAudio",
        NEXT_AUDIO: "NextAudio",
        AUDIO_SPEED_CONTROLLER: "AudioSpeedController",
        VOLUME: "Volume",
        TIMER: "Timer",
        AUDIO_ITEM: "AudioItem"
    };


    var classList = {
        addonWrapper: 'wrapper-addon-audio-playlist',
        controls: 'addon-audio-playlist-controls',
        prev: 'audio-playlist-prev-btn',
        next: 'audio-playlist-next-btn',
        playPauseButton: 'audio-playlist-play-pause-btn',
        playButton: 'audio-playlist-play-btn',
        pauseButton: 'audio-playlist-pause-btn',
        timer: 'audio-playlist-timer',
        bar: 'audio-playlist-bar',
        barFilling: 'audio-playlist-bar--fill',
        barBall: 'addon-audio-playlist-bar--ball',
        duration: 'audio-playlist-max-time',
        volume: 'audio-playlist-volume-btn',
        items: 'addon-audio-playlist-items',
        item: 'addon-audio-playlist-item',
        itemName: 'addon-audio-playlist-item--name',
        itemSelected: 'addon-audio-playlist-item-selected',
        itemButton: 'addon-audio-playlist-item--button',
        itemPlay: 'addon-audio-playlist-item--button-playing',
        volumeWrapper: 'addon-audio-playlist-volume-wrapper',
        volumeWrapperExpanded: 'addon-audio-playlist-volume-wrapper--expanded',
        volumeBar: 'addon-audio-playlist-volume-bar',
        volumeBarHidden: 'addon-audio-playlist-volume-bar--hidden',
        volumeBarFill: 'addon-audio-playlist-volume-bar-fill',
        audioSpeedController: 'audio-speed-controller'
    };

    var eventNames = {
        playing: "playing",
        pause: "pause",
        end: "end",
        next: "next"
    };
    var operationType = {
        increase: 'increase',
        decrease: 'decrease'
    }
    var playbackRateList = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];

    presenter.playbackRate = 1.0;
    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.wrapper = null;
    presenter.viewItems = {
        prevButton: null,
        nextButton: null,
        playPauseButton: null,
        currentTime: null,
        timerBar: null,
        timerSlider: null,
        timerSliderBall: null,
        maxTime: null,
        volumeButton: null,
        items: null,
        volumeWrapper: null,
        volumeBar: null,
        volumeBarFill: null,
        audioSpeedController: null
    };
    presenter.keyboardControllerObject = null;
    presenter.selectedElement = null;
    presenter.isWCAGOn = false;
    presenter.speechTexts = null;

    presenter.dragData = {
        wasPlaying: false,
        started: false,
        position: 0
    };

    presenter.items = []; // items in the playlist

    presenter.state = {
        isVisible: false,
        isPlaying: false,
        currentItemIndex: 0,
        sentTime: "00:00",
        showingVolume: false
    };

    presenter.setPlayerController = function AddonAudioPlaylist_setPlayerController(controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.run = function AddonAudioPlaylist_run(view, model) {
        presenter.initialize(view, model, false);
        eventBus = presenter.playerController.getEventBus();
        eventBus.addEventListener('ValueChanged', this);
    };

    presenter.createPreview = function AddonAudioPlaylist_createPreview(view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.initialize = function AddonAudioPlaylist_initialize(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.view = view;
        var validatedModel = presenter.validateModel(upgradedModel);
        presenter.setSpeechTexts(model["speechTexts"]);
        this.assignViewItems(view);

        if (!validatedModel.isValid) {
            presenter.showValidationError(validatedModel);
            return;
        }

        presenter.configuration = validatedModel.value;
        presenter.state.isVisible = presenter.configuration.isVisible;

        presenter.createItems();
        if (!isPreview) {
            presenter.audio = presenter.view.getElementsByTagName("audio")[0];
            presenter.addHandlers();

            if (MobileUtils.isSafariMobile(navigator.userAgent)) {
                presenter.viewItems.volumeButton.style.visibility = "hidden";
            }
        }

        updateBallPosition();
        presenter.selectItem(this.state.currentItemIndex);
        presenter.buildKeyboardController();
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradePlaybackSpeedControls(model);
        upgradedModel = presenter.upgradeLangAttribute(upgradedModel);
        upgradedModel = presenter.upgradeSpeechText(upgradedModel);

        return upgradedModel;
    }

    presenter.upgradePlaybackSpeedControls = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('Enable audio speed controller')) {
            upgradedModel['Enable audio speed controller'] = 'False';
        }

        return upgradedModel;
    }

    presenter.upgradeLangAttribute = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('langAttribute')) {
            upgradedModel['langAttribute'] = '';
        }

        return upgradedModel;
    }

    presenter.upgradeSpeechText = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('speechTexts')) {
            upgradedModel['speechTexts'] = {};
        }

        var speechTextProperties = ['play', 'pause', 'prev', 'next', 'audioSpeedController', 'volume', 'timer', 'audioItem'];
        speechTextProperties.forEach((propertyName) =&gt; {
            if (!upgradedModel['speechTexts'].hasOwnProperty(propertyName)) {
                var property = {};
                property[propertyName] = "";
                upgradedModel['speechTexts'][propertyName] = property;
            }
        });

        return upgradedModel;
    }

    presenter.getViewItemsWithClickAndTouchHandlers = function AddonAudioPlaylist_getViewItemsWithClickAndTouchHandlers() {
        return [
            { item: presenter.viewItems.playPauseButton, handler: AddonAudioPlaylist__playPauseButtonHandler },
            { item: presenter.viewItems.prevButton, handler: AddonAudioPlaylist__prevButtonHandler },
            { item: presenter.viewItems.nextButton, handler: AddonAudioPlaylist__nextButtonHandler },
            { item: presenter.viewItems.volumeButton, handler: AddonAudioPlaylist__volumeButtonHandler },
            { item: presenter.viewItems.volumeBar, handler: AddonAudioPlaylist__volumeBarHandler }
        ];
    };

    presenter.getAudioHandlers = function AddonAudioPlaylist_getAudioHandlers() {
        return [
            { event: 'loadeddata', handler: AddonAudioPlaylist__onLoadedMetadataCallback },
            { event: 'durationchange', handler: AddonAudioPlaylist__onTimeDurationLoadedCallback },
            { event: 'timeupdate', handler: AddonAudioPlaylist__onTimeUpdateCallback },
            { event: 'ended', handler: AddonAudioPlaylist___onAudioEnded },
            { event: 'playing', handler: AddonAudioPlaylist___onAudioPlaying },
            { event: 'pause', handler: AddonAudioPlaylist___onAudioPause }
        ]
    };

    presenter.initKeyboardNavigationHandlers = function AddonPlaylist_initKeyboardNavigationHandlers() {
        presenter.keyboardControllerObject.mapping[KeyboardControllerKeys.ARROW_LEFT] = presenter.keyboardControllerObject.left;
        presenter.keyboardControllerObject.mapping[KeyboardControllerKeys.ARROW_RIGHT] = presenter.keyboardControllerObject.right;
        presenter.keyboardControllerObject.mapping[KeyboardControllerKeys.ARROW_UP] = presenter.keyboardControllerObject.up;
        presenter.keyboardControllerObject.mapping[KeyboardControllerKeys.ARROW_DOWN] = presenter.keyboardControllerObject.down;
        presenter.keyboardControllerObject.mapping[KeyboardControllerKeys.TAB] = presenter.keyboardControllerObject.tab;
    }

    presenter.buildKeyboardController = function AddonAudioPlaylist_buildKeyboardController() {
        var elements = [];
        presenter.getViewItemsWithClickAndTouchHandlers().forEach(function (item) {
            if (!item.item.className.includes('volume-bar')) {
                elements.push($(item.item));
            }
        });

        elements.splice(3, 0, $(presenter.viewItems.timerBar));

        if(presenter.configuration.enableAudioSpeedController) {
            elements.splice(4, 0, $(presenter.viewItems.audioSpeedController));
        }

        presenter.items.forEach((audio) =&gt; {
            elements.push($(audio.row));
        })

        presenter.keyboardControllerObject = new AudioPlaylistKeyboardController(elements);
        presenter.keyboardControllerObject.selectEnabled(true);

        presenter.initKeyboardNavigationHandlers();
    }

    function AudioPlaylistKeyboardController(elements) {
        KeyboardController.call(this, elements, elements.length);
    }

    AudioPlaylistKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    AudioPlaylistKeyboardController.prototype.constructor = AudioPlaylistKeyboardController;

    AudioPlaylistKeyboardController.prototype.tab = function (event) {
        this.closeVolumeBar();
        KeyboardController.prototype.nextElement.call(this, event);
        presenter.selectedElement = null;
        this.readCurrentElement();
    }

    AudioPlaylistKeyboardController.prototype.closeVolumeBar = function () {
        const isVolumeBarClosed = presenter.viewItems.volumeBar.classList.contains(classList.volumeBarHidden);
        const isVolumeElementSelected = presenter.selectedElement === presenter.NAVIGATION_ELEMENT.VOLUME;
        if (isVolumeElementSelected &amp;&amp; !isVolumeBarClosed) {
            this.keyboardNavigationCurrentElement[0].click();
        }
    }

    AudioPlaylistKeyboardController.prototype.previousElement = function (event) {
        this.closeVolumeBar();
        KeyboardController.prototype.previousElement.call(this, event);
        presenter.selectedElement = null;
        this.readCurrentElement();
    }

    AudioPlaylistKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    }

    AudioPlaylistKeyboardController.prototype.select = function (event) {
        event.preventDefault();
        if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.VOLUME) return;

        var self = this;
        var promise = new Promise(function (resolve) {
                resolve(presenter.selectElement(self.keyboardNavigationCurrentElement[0]));
            });
        promise.then(function () {
            if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.AUDIO_ITEM) {
                self.keyboardNavigationCurrentElement[0].firstChild.click();
            } else {
                self.keyboardNavigationCurrentElement[0].click();
            }
        });
    }

    AudioPlaylistKeyboardController.prototype.up = function (event) {
        event.preventDefault();
        if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.AUDIO_SPEED_CONTROLLER) {
            presenter.changePlaybackRate(operationType.increase);
        } else if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.VOLUME) {
            presenter.changeVolume(operationType.increase);
        }
    }

    AudioPlaylistKeyboardController.prototype.down = function (event) {
        event.preventDefault();
        if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.AUDIO_SPEED_CONTROLLER) {
            presenter.changePlaybackRate(operationType.decrease);
        } else if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.VOLUME) {
            presenter.changeVolume(operationType.decrease);
        }
    }

    presenter.changeVolume = function (type) {
        var volume = presenter.audio.volume;

        switch (type) {
            case operationType.increase:
                volume += 0.1;
                if (volume &gt; 1.0) volume = 1.0;
                break;
            case operationType.decrease:
                volume -= 0.1;
                if (volume &lt; 0.0) volume = 0.0;
                break;
        }

        presenter.audio.volume = volume;
        var percent = Math.round(volume * 100);
        presenter.viewItems.volumeBarFill.style.width = `${percent}%`;
    }

    presenter.changePlaybackRate = function (type) {
        var index = playbackRateList.indexOf(presenter.playbackRate);
        var $select = $(presenter.viewItems.audioSpeedController).find('select');

        switch (type) {
            case operationType.increase:
                if (index === (playbackRateList.length - 1)) return;
                index++;
                break;
            case operationType.decrease:
                if (index === 0) return;
                index--;
                break;
        }

        $select.val(playbackRateList[index]);
        presenter.playbackRate = playbackRateList[index];
        presenter.audio.playbackRate = presenter.playbackRate
    }

    AudioPlaylistKeyboardController.prototype.left = function (event) {
        event.preventDefault();
        if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.TIMER) {
            presenter.audio.currentTime -= 5;
        }
    }

    AudioPlaylistKeyboardController.prototype.right = function (event) {
        event.preventDefault();
        if (presenter.selectedElement === presenter.NAVIGATION_ELEMENT.TIMER) {
            presenter.audio.currentTime += 5;
        }
    }

    AudioPlaylistKeyboardController.prototype.escape = function (event) {
        presenter.pause();
        this.closeVolumeBar();

        if (!presenter.audio) presenter.audio.currentTime = 0;
        presenter.selectedElement = null;

        KeyboardController.prototype.escape.call(this, event);
    }

    AudioPlaylistKeyboardController.prototype.exitWCAGMode = function (event) {
        this.closeVolumeBar();
        presenter.selectedElement = null;

        KeyboardController.prototype.exitWCAGMode.call(this, event);
    }

    presenter.getCurrentElement = function(elementHTML) {
        var elementHTML = elementHTML.outerHTML;
        switch (true) {
            case (elementHTML.includes(classList.playPauseButton) &amp;&amp; !presenter.state.isPlaying):
                return presenter.NAVIGATION_ELEMENT.PLAY;
            case (elementHTML.includes(classList.playPauseButton &amp;&amp; presenter.state.isPlaying)):
                return presenter.NAVIGATION_ELEMENT.PAUSE;
            case (elementHTML.includes(classList.prev)):
                return presenter.NAVIGATION_ELEMENT.PREVIOUS_AUDIO;
            case (elementHTML.includes(classList.next)):
                return presenter.NAVIGATION_ELEMENT.NEXT_AUDIO;
            case (elementHTML.includes(classList.volume)):
                return presenter.NAVIGATION_ELEMENT.VOLUME;
            case (elementHTML.includes(classList.audioSpeedController)):
                return presenter.NAVIGATION_ELEMENT.AUDIO_SPEED_CONTROLLER;
            case (elementHTML.includes(classList.bar)):
                return presenter.NAVIGATION_ELEMENT.TIMER;
            case (elementHTML.includes(classList.item)):
                return presenter.NAVIGATION_ELEMENT.AUDIO_ITEM;
            default:
                return null;
        }
    }

    presenter.selectElement = function (elementHTML) {
        presenter.selectedElement = presenter.getCurrentElement(elementHTML);
        return true;
    }

    presenter.keyboardController = function (keycode, isShift, event) {
        presenter.keyboardControllerObject.handle(keycode, isShift, event);
    }

    presenter.getTextToSpeechOrNull = function () {
        if (presenter.playerController) {
            return presenter.playerController.getModule('Text_To_Speech1');
        }
        return null;
    };

    presenter.setWCAGStatus = function (isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    }

    presenter.speak = function (data) {
        var tts = presenter.getTextToSpeechOrNull();
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    AudioPlaylistKeyboardController.prototype.readCurrentElement = function () {
        if (!presenter.isWCAGOn) return;

        var $element = this.getTarget(this.keyboardNavigationCurrentElement, false);
        var textToRead = presenter.getTextToRead($element);

        presenter.speak(textToRead);
    }

    presenter.getTextToRead = function ($element) {
        var currentElement = presenter.getCurrentElement($element[0]);
        var textToRead = presenter.speechTexts[currentElement];

        if (currentElement === presenter.NAVIGATION_ELEMENT.AUDIO_ITEM) {
            var audioTitle = getAudioTitle($element);
            textToRead = `${presenter.getAudioIndex(audioTitle) + 1} ${textToRead}`;
            return [TTSUtils.getTextVoiceObject(textToRead), TTSUtils.getTextVoiceObject(audioTitle, presenter.configuration.langAttribute)];
        }
        return [TTSUtils.getTextVoiceObject(textToRead)];
    }

    function getAudioTitle($element) {
        return $element[0].innerText.split(/\n/)[0];
    }

    presenter.getAudioIndex = function (audioTitle) {
        for(var i = 0; i &lt; presenter.items.length; i++) {
            if (presenter.items[i].name === audioTitle) {
                return presenter.items[i].index;
            }
        }
    }

    presenter.destroy = function AddonAudioPlaylist_destroy() {
        presenter.playerController = null;
        var i;

        var elements = presenter.getViewItemsWithClickAndTouchHandlers();

        for (i = 0; i &lt; elements.length; i++) {
            elements[i].item.removeEventListener('click', elements[i].handler);
            elements[i].item.removeEventListener('touch', elements[i].handler);
        }

        elements = presenter.getAudioHandlers();

        for (i = 0; i &lt; elements.length; i++) {
            presenter.audio.removeEventListener(elements[i].event, elements[i].handler);
        }

        presenter.pause();
        presenter.audio = null;

        for (i = 0; i &lt; presenter.items.length; i++) {
            presenter.items[i].button.removeEventListener('durationchange', AddonAudioPlaylistItemWrapper__buttonHandler);

            if (presenter.items[i].audio) {
                presenter.items[i].audio.removeEventListener('durationchange', AddonAudioPlaylistItemWrapper__audioDurationChange);
                presenter.items[i].audio = null;
            }
        }

        deferredSyncQueue = null;
    };

    presenter.onEventReceived = function AddonAudioPlaylist_onEventReceived(eventName, eventData) {    };


    presenter.show = function AddonAudioPlaylist_show() {
        this.setVisibility(true);
        this.state.isVisible = true;
    };

    presenter.hide = function AddonAudioPlaylist_hide() {
        this.stop();
        this.setVisibility(false);
        this.state.isVisible = false;
    };

    presenter.reset = function AddonAudioPlaylist_reset() {
        if (!presenter.audio) return;

        presenter.pause();

        presenter.state.isVisible = presenter.configuration.isVisible;
        if (presenter.state.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };

    /**
     * @return {string}
     */
    presenter.getState = function AddonAudioPlaylist_getState() {
        return JSON.stringify({
            isVisible: presenter.state.isVisible,
            currentItemIndex: presenter.state.currentItemIndex
        });
    };

    presenter.setState = function AddonAudioPlaylist_setState(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) {
            return;
        }

        presenter.stop();

        var state = JSON.parse(stateString);

        if (state.isVisible) {
            this.show();
        } else {
            this.hideAddon();
        }

        presenter.changeItem(state.currentItemIndex, false);
    };

    presenter.validateModel = function AddonAudioPlaylist_validateModel(model) {
        var modelValidator = new ModelValidator();

        return modelValidator.validate(model, [
            ModelValidators.String("ID"),
            ModelValidators.utils.FieldRename(
                "Is Visible", "isVisible", ModelValidators.Boolean("isVisible")
            ),
            ModelValidators.utils.FieldRename(
                "Items",
                "items",
                ModelValidators.List("items", [
                    ModelValidators.utils.FieldRename("Name", "name", ModelValidators.String("name")),
                    ModelValidators.utils.FieldRename("Mp3", "mp3", ModelValidators.String("mp3", {default: ""})),
                    ModelValidators.utils.FieldRename("Ogg", "ogg", ModelValidators.String("ogg", {default: ""}))
                ])
            ),
            ModelValidators.utils.FieldRename("Stop playing", "stopPlaying", ModelValidators.Boolean("stopPlaying")),
            ModelValidators.utils.FieldRename("Enable audio speed controller", "enableAudioSpeedController",
                ModelValidators.Boolean("enableAudioSpeedController", {default: false})),
            ModelValidators.String("langAttribute", {
                trim: true,
                default: ""
            })
        ]);
    };

    presenter.setSpeechTexts = function (speechText) {
        presenter.speechTexts = {
            Play: presenter.DEFAULT_TTS_PHRASES.PLAY,
            Pause: presenter.DEFAULT_TTS_PHRASES.PAUSE,
            PreviousAudio: presenter.DEFAULT_TTS_PHRASES.PREVIOUS_AUDIO,
            NextAudio: presenter.DEFAULT_TTS_PHRASES.NEXT_AUDIO,
            AudioSpeedController: presenter.DEFAULT_TTS_PHRASES.AUDIO_SPEED_CONTROLLER,
            Volume: presenter.DEFAULT_TTS_PHRASES.VOLUME,
            Timer: presenter.DEFAULT_TTS_PHRASES.TIMER,
            AudioItem: presenter.DEFAULT_TTS_PHRASES.AUDIO_ITEM
        };

        if(!speechText) return;

        presenter.speechTexts = {
            Play: TTSUtils.getSpeechTextProperty(
                speechText.play.play,
                presenter.speechTexts.Play),
            Pause: TTSUtils.getSpeechTextProperty(
                speechText.pause.pause,
                presenter.speechTexts.Pause),
            PreviousAudio: TTSUtils.getSpeechTextProperty(
                speechText.prev.prev,
                presenter.speechTexts.PreviousAudio),
            NextAudio: TTSUtils.getSpeechTextProperty(
                speechText.next.next,
                presenter.speechTexts.NextAudio),
            AudioSpeedController: TTSUtils.getSpeechTextProperty(
                speechText.audioSpeedController.audioSpeedController,
                presenter.speechTexts.AudioSpeedController),
            Volume: TTSUtils.getSpeechTextProperty(
                speechText.volume.volume,
                presenter.speechTexts.Volume),
            Timer: TTSUtils.getSpeechTextProperty(
                speechText.timer.timer,
                presenter.speechTexts.Timer),
            AudioItem: TTSUtils.getSpeechTextProperty(
                speechText.audioItem.audioItem,
                presenter.speechTexts.AudioItem)
        };
    };

    presenter.executeCommand = function AddonAudioPlaylist_executeCommand(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'play': presenter.play,
            'pause': presenter.pause,
            'stop': presenter.stop,
            'jumpTo': presenter.changeItem,
            'previous': presenter.prev,
            'next': presenter.next,
            'setPlaybackRate': presenter.setPlaybackRate
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function AddonAudioPlaylist_setVisibility(isVisible) {
        presenter.view.style.visibility = isVisible ? "visible" : "hidden";
    };

    presenter.play = function () {
        if (!presenter.audio) return;
        if (presenter.audio.src &amp;&amp; presenter.audio.paused) {
            presenter.audio.play();
            presenter.viewItems.playPauseButton.classList.remove(classList.playButton);
            presenter.viewItems.playPauseButton.classList.add(classList.pauseButton);
            presenter.items[presenter.state.currentItemIndex].button.classList.add(classList.itemPlay);
            presenter.state.isPlaying = true;
            presenter.audio.playbackRate = presenter.playbackRate;
        }
    };

    presenter.pause = function AddonAudioPlaylist_pause() {
        if (!presenter.audio) return;
        if (presenter.audio.readyState &gt; 0) {
            if (!presenter.audio.paused) {
                presenter.audio.pause();
            }

            presenter.viewItems.playPauseButton.classList.add(classList.playButton);
            presenter.viewItems.playPauseButton.classList.remove(classList.pauseButton);
            presenter.items[presenter.state.currentItemIndex].button.classList.remove(classList.itemPlay);

            presenter.state.isPlaying = false;
        }
    };

    presenter.stop = function AddonAudioPlaylist_stop() {
        if (!presenter.audio) return;
        if (presenter.audio.readyState &gt; 0) {
            presenter.pause();
            presenter.audio.currentTime = 0;
        }
    };

    presenter.setPlaybackRate = function (value) {
        if (!presenter.audio) return;
        if (isNaN(value)) return;
        var parsedValue = parseFloat(value);
        presenter.playbackRate = parsedValue;
        presenter.audio.playbackRate = parsedValue;
        displayPlaybackRate();
    };

    presenter.assignViewItems = function (view) {
        presenter.wrapper = view.getElementsByClassName(classList.addonWrapper)[0];
        presenter.viewItems = {
            mainController: view.getElementsByClassName(classList.controls)[0],
            prevButton: view.getElementsByClassName(classList.prev)[0],
            nextButton: view.getElementsByClassName(classList.next)[0],
            playPauseButton: view.getElementsByClassName(classList.playPauseButton)[0],
            currentTime: view.getElementsByClassName(classList.timer)[0],
            timerBar: view.getElementsByClassName(classList.bar)[0],
            timerSlider: view.getElementsByClassName(classList.barFilling)[0],
            timerSliderBall: view.getElementsByClassName(classList.barBall)[0],
            maxTime: view.getElementsByClassName(classList.duration)[0],
            volumeButton: view.getElementsByClassName(classList.volume)[0],
            items: view.getElementsByClassName(classList.items)[0],
            volumeWrapper: view.getElementsByClassName(classList.volumeWrapper)[0],
            volumeBar: view.getElementsByClassName(classList.volumeBar)[0],
            volumeBarFill: view.getElementsByClassName(classList.volumeBarFill)[0],
            audioSpeedController: view.getElementsByClassName(classList.audioSpeedController)[0]
        };
    };

    function createPlaybackRateSelectElement() {
        var $select = $('&lt;select&gt;');
        for (var i = 0; i &lt; playbackRateList.length; i++) {
            var $option = $('&lt;option&gt;');
            $option.text(playbackRateList[i]);
            $option.attr('value', playbackRateList[i]);
            if (playbackRateList[i] === 1.0) {
                $option.attr('selected', 'selected');
            }
            $select.append($option);
        }

        $select.on('change', function(){
            presenter.setPlaybackRate($select.val());
        })

        $(presenter.viewItems.audioSpeedController).append($select);
    }

    function displayPlaybackRate () {
        $(presenter.viewItems.audioSpeedController).css('display', 'block');

        var $select = $(presenter.viewItems.audioSpeedController).find('select');
        if ($select.val() === presenter.playbackRate) {
            if (playbackRateList.indexOf(presenter.playbackRate) !== -1) {
                $select.find('.custom-option').remove();
            }
            return;
        }
        $select.find('.custom-option').remove();
        if (playbackRateList.indexOf(presenter.playbackRate) !== -1) {
            $select.val(presenter.playbackRate);
        } else {
            var $customOption = $('&lt;option&gt;');
            $customOption.text(presenter.playbackRate);
            $customOption.attr('value', presenter.playbackRate);
            $customOption.addClass('custom-option');
            $select.append($customOption);
            $select.val(presenter.playbackRate);
        }
    }

    presenter.showValidationError = function AddonAudioPlaylist_showValidationError(errorModel) {
        presenter.viewItems.mainController.style.visibility = "hidden";
        presenter.viewItems.items.style.visibility = "visible";
        DOMOperationsUtils.showErrorMessage(presenter.viewItems.items, presenter.ERROR_CODES, errorModel.fieldName.join("|") + "_" + errorModel.errorCode);
    };

    presenter.createItems = function AddonAudioPlaylist_createItems() {
        presenter.items = presenter.configuration.items.map(function (item, index) {
            return new AddonAudioPlaylistItemWrapper(item, index);
        });

        presenter.items.forEach(function (item) {
            presenter.viewItems.items.appendChild(item.row);
        });

        if (presenter.configuration.enableAudioSpeedController) {
            createPlaybackRateSelectElement();
            displayPlaybackRate();
        }
    };

    presenter.selectItem = function AddonAudioPlaylist_selectItem(index) {
        if (index &lt; 0 || index &gt; this.items.length - 1) {
            return false;
        }

        presenter.pause();
        presenter.items[presenter.state.currentItemIndex].row.classList.remove(classList.itemSelected);
        presenter.items[presenter.state.currentItemIndex].button.classList.remove(classList.itemPlay);

        presenter.state.currentItemIndex = index;
        if (presenter.audio) {
            presenter.audio.src = this.items[presenter.state.currentItemIndex].src;
        }
        presenter.items[presenter.state.currentItemIndex].row.classList.add(classList.itemSelected);
        presenter.sendEvent(
        "ValueChanged",
        {
            value: eventNames.next,
            item: presenter.state.currentItemIndex,
            source: presenter.configuration.ID,
            score: ""
        });

        return true;
    }

    presenter.changeItem = function AddonAudioPlaylist_changeItem(index, startPlaying = true) {
        var wasSelected = presenter.selectItem(index);

        if (wasSelected &amp;&amp; !presenter.configuration.stopPlaying &amp;&amp; startPlaying) {
            presenter.play();
        }

        return wasSelected;
    };

    presenter.addHandlers = function AddonAudioPlaylist_addHandlers() {
        var elements = presenter.getViewItemsWithClickAndTouchHandlers();

        for (var i = 0; i &lt; elements.length; i++) {
            elements[i].item.addEventListener('click', elements[i].handler);
            elements[i].item.addEventListener('touch', elements[i].handler);
        }

        elements = presenter.getAudioHandlers();

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            presenter.viewItems.timerSliderBall.addEventListener('touchstart', AddonAudioPlaylist__sliderTouchDragStartHandler);
            presenter.viewItems.timerBar.addEventListener('touchmove', AddonAudioPlaylist__sliderTouchDragMoveHandler);
            presenter.viewItems.timerBar.addEventListener('touchend', AddonAudioPlaylist__sliderDragStopHandler);
        } else {
            presenter.viewItems.timerSliderBall.addEventListener('mousedown', AddonAudioPlaylist__sliderMouseDragStartHandler);
            presenter.viewItems.timerBar.addEventListener('mousemove', AddonAudioPlaylist__sliderMouseDragMoveHandler);
            presenter.viewItems.timerBar.addEventListener('mouseup', AddonAudioPlaylist__sliderDragStopHandler);
        }

        for (i = 0; i &lt; elements.length; i++) {
            presenter.audio.addEventListener(elements[i].event, elements[i].handler);
        }

        // adds the audio duration
        presenter.items.forEach(function (item) {
            item.audio = document.createElement("audio");
            item.audio.addEventListener("durationchange", AddonAudioPlaylistItemWrapper__audioDurationChange.bind(item));
            item.audio.src = item.src;
        });
    };

    presenter.updateMainTrackDuration = function AddonAudioPlaylist_updateMainTrackDuration(duration) {
        duration = isNaN(duration) ? 0 : duration;
        presenter.viewItems.maxTime.innerText = StringUtils.timeFormat(duration);
    };

    presenter.next = function () {
        return presenter.changeItem(presenter.state.currentItemIndex + 1);
    };

    presenter.prev = function () {
        return presenter.changeItem(presenter.state.currentItemIndex - 1);
    };

    presenter.sendEvent = function (name, data) {
        if (presenter.eventBus) {
            presenter.eventBus.sendEvent(name, data);
        }
    };

    function AddonAudioPlaylist__nextButtonHandler(ev) {
        ev.preventDefault();
        presenter.next();
    }

    function AddonAudioPlaylist__prevButtonHandler(ev) {
        ev.preventDefault();
        if (presenter.audio.readyState &gt; 0 &amp;&amp; presenter.audio.currentTime &gt; 0) {
            presenter.pause();
            presenter.audio.currentTime = 0;
        } else {
            presenter.prev();
        }
    }

    function AddonAudioPlaylist__playPauseButtonHandler(ev) {
        ev.preventDefault();
        if (presenter.state.isPlaying) {
            presenter.pause();
        } else {
            presenter.play();
        }
    }

    function AddonAudioPlaylist__onTimeDurationLoadedCallback() {
        presenter.updateMainTrackDuration(presenter.audio.duration);
    }

    function AddonAudioPlaylist__onLoadedMetadataCallback() {
        AddonAudioPlaylist__onTimeUpdateCallback();
        presenter.updateMainTrackDuration(presenter.audio.duration);
    }

    function AddonAudioPlaylist__onTimeUpdateCallback() {
        var currentTime = presenter.audio.currentTime;
        var duration = isNaN(presenter.audio.duration) ? 1 : presenter.audio.duration;
        var fillPercent = Math.round(currentTime / duration * 100);

        var time = StringUtils.timeFormat(currentTime);
        presenter.viewItems.currentTime.innerText = time;

        presenter.viewItems.timerSlider.style.width = fillPercent + "%";
        updateBallPosition();

        if (time !== presenter.state.sentTime) {
            presenter.sendEvent("ValueChanged", {
                value: time,
                item: presenter.state.currentItemIndex,
                source: presenter.configuration.ID,
                score: ""
            });

            presenter.state.sentTime = time;
        }
    }

    function updateBallPosition() {
        var halvedBallWidth = presenter.viewItems.timerSliderBall.offsetWidth / 2;
        var timerFillWidth = presenter.viewItems.timerSlider.offsetWidth;
        var ballPosition =  timerFillWidth- halvedBallWidth;

        if (timerFillWidth &lt; halvedBallWidth) {
            ballPosition = 0;
        }

        presenter.viewItems.timerSliderBall.style.left = ballPosition + "px";
    }

    function AddonAudioPlaylist___onAudioEnded() {
        presenter.pause();

        presenter.sendEvent("ValueChanged", {
            value: eventNames.end,
            item: presenter.state.currentItemIndex,
            source: presenter.configuration.ID,
            score: ""
        });
        presenter.next();
    }

    function AddonAudioPlaylist___onAudioPlaying() {
        presenter.sendEvent("ValueChanged", {
            value: eventNames.playing,
            item: presenter.state.currentItemIndex,
            source: presenter.configuration.ID,
            score: ""
        });
    }

    function AddonAudioPlaylist___onAudioPause() {
        presenter.sendEvent("ValueChanged", {
            value: eventNames.pause,
            item: presenter.state.currentItemIndex,
            source: presenter.configuration.ID,
            score: ""
        });
    }

    function AddonAudioPlaylist__volumeButtonHandler(ev) {
        if (presenter.state.showingVolume) {
            presenter.viewItems.volumeWrapper.classList.remove(classList.volumeWrapperExpanded);
            presenter.viewItems.volumeBar.classList.add(classList.volumeBarHidden);
        } else {
            presenter.viewItems.volumeWrapper.classList.add(classList.volumeWrapperExpanded);
            presenter.viewItems.volumeBar.classList.remove(classList.volumeBarHidden);
        }

        presenter.state.showingVolume = !presenter.state.showingVolume;
        updateBallPosition();
    }

    function AddonAudioPlaylist__volumeBarHandler(ev) {
        var width = presenter.viewItems.volumeBar.offsetWidth;
        var clickedWidth = ev.offsetX;

        var value = clickedWidth / width;
        var percent = Math.round(value * 100);
        presenter.viewItems.volumeBarFill.style.width = percent + "%";

        if (presenter.audio) {
            presenter.audio.volume = value;
        }
    }

    function AddonAudioPlaylist__sliderMouseDragStartHandler(ev) {
        AddonAudioPlaylist__sliderDragStartHandler(ev.pageX);
    }

    function AddonAudioPlaylist__sliderMouseDragMoveHandler(ev) {
        AddonAudioPlaylist__sliderDragMoveHandler(ev.pageX);
    }

    function AddonAudioPlaylist__sliderTouchDragStartHandler(ev) {
        var pageX = ev.changedTouches ? ev.changedTouches[0].pageX : ev.pageX;
        AddonAudioPlaylist__sliderDragStartHandler(pageX || 0);
    }

    function AddonAudioPlaylist__sliderTouchDragMoveHandler(ev) {
        var pageX = ev.changedTouches ? ev.changedTouches[0].pageX : ev.pageX;
        AddonAudioPlaylist__sliderDragMoveHandler(pageX || 0);
    }

    function AddonAudioPlaylist__sliderDragStartHandler(pageX) {
        presenter.dragData.wasPlaying = presenter.state.isPlaying;
        presenter.dragData.started = true;
        presenter.dragData.position = pageX;

        presenter.pause();
    }

    function AddonAudioPlaylist__sliderDragMoveHandler(pageX) {
        if (!presenter.dragData.started) return;

        var barWidth = presenter.viewItems.timerBar.offsetWidth;

        var currentFillWidth = presenter.viewItems.timerSlider.offsetWidth;
        var change = pageX - presenter.dragData.position;
        if (currentFillWidth + change &gt;= 0 &amp;&amp; currentFillWidth + change &lt;= barWidth) {
            var percent = (currentFillWidth + change) / barWidth * 100;
            var value = currentFillWidth / barWidth;
            presenter.viewItems.timerSlider.style.width = percent + "%";
            updateBallPosition();

            var estimatedTime = Math.round(presenter.audio.duration * value);
            presenter.viewItems.currentTime.innerText = StringUtils.timeFormat(estimatedTime);
        }

        presenter.dragData.position = pageX;
    }

    function AddonAudioPlaylist__sliderDragStopHandler(ev) {
        if (!presenter.dragData.started) return;

        if (presenter.audio.readyState &gt; 0) {
            var barWidth = presenter.viewItems.timerBar.offsetWidth;
            var currentFillWidth = presenter.viewItems.timerSlider.offsetWidth;
            var percent = currentFillWidth / barWidth;

            presenter.audio.currentTime = Math.round(presenter.audio.duration * percent);
        }
        presenter.dragData.started = false;
        if (presenter.dragData.wasPlaying) {
            presenter.play();
            presenter.dragData.wasPlaying = false;
        }
    }

    /*
    Wrapper for audio item
    * */
    function AddonAudioPlaylistItemWrapper(item, index) {
        var row = document.createElement("div");
        var playButton = document.createElement("button");
        var name = document.createElement("span");
        var time = document.createElement("span");

        row.classList.add(classList.item);
        name.classList.add(classList.itemName);
        playButton.classList.add(classList.itemButton);
        playButton.classList.add(classList.playButton);

        row.appendChild(playButton);
        row.appendChild(name);
        row.appendChild(time);

        name.innerText = item.name;
        time.innerText = "00:00";

        playButton.addEventListener("click", AddonAudioPlaylistItemWrapper__buttonHandler.bind(this));
        playButton.addEventListener("touch", AddonAudioPlaylistItemWrapper__buttonHandler.bind(this));

        this.name = item.name;
        this.src = item.mp3 || item.ogg;
        this.button = playButton;
        this.row = row;
        this.time = time;
        this.index = index;
        this.audio = null;
    }

     function AddonAudioPlaylistItemWrapper__audioDurationChange(ev) {
        this.time.innerText = StringUtils.timeFormat(isNaN(this.audio.duration) ? 0 : this.audio.duration);
        this.audio.removeEventListener("durationchange", AddonAudioPlaylistItemWrapper__audioDurationChange);
        this.audio = null;
    }

    function AddonAudioPlaylistItemWrapper__buttonHandler(ev) {
        ev.preventDefault();
        if (presenter.state.currentItemIndex !== this.index) {
            presenter.changeItem(this.index);
        }
        if (presenter.state.isPlaying) {
            presenter.pause();
        } else {
            presenter.play();
        }
    }

    return presenter;
}

AddonAudioPlaylist_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Automatic_Feedback" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="ActivityModuleID" nameLabel="Automatic_Feedback_property_activity_module_id" type="string"/>
		<property name="ActivityType" nameLabel="Automatic_Feedback_property_activity_type" type="{Default}"/>
		<property name="CorrectFeedback" nameLabel="Automatic_Feedback_property_correct_feedback" type="list">
			<property name="ActivityItem" nameLabel="Automatic_Feedback_property_activity_item" type="string"/>
			<property name="Feedback" nameLabel="Automatic_Feedback_property_feedback" type="html"/>
		</property>
		<property name="IncorrectFeedback" nameLabel="Automatic_Feedback_property_incorrect_feedback" type="list">
			<property name="ActivityItem" nameLabel="Automatic_Feedback_property_activity_item" type="string"/>
			<property name="Feedback" nameLabel="Automatic_Feedback_property_feedback" type="html"/>
		</property>
		<property name="EmptyFeedback" nameLabel="Automatic_Feedback_property_empty_feedback" type="list">
			<property name="ActivityItem" nameLabel="Automatic_Feedback_property_activity_item" type="string"/>
			<property name="Feedback" nameLabel="Automatic_Feedback_property_feedback" type="html"/>
		</property>
		<property name="PartialFeedback" nameLabel="Automatic_Feedback_property_partial_feedback" type="list">
			<property name="ActivityItem" nameLabel="Automatic_Feedback_property_activity_item" type="string"/>
			<property name="Feedback" nameLabel="Automatic_Feedback_property_feedback" type="html"/>
		</property>
		<property name="ResetResponseOnPageChange" nameLabel="Automatic_Feedback_property_reset_response_on_page_change" type="boolean"/>
		<property name="Display" nameLabel="Automatic_Feedback_property_display_mode" type="{block,tooltips,popup}"/>
		<property name="DisplayFeedbackButtons" nameLabel="Automatic_Feedback_property_display_feedback_buttons" type="boolean"/>
		<property name="ReactTo" nameLabel="Automatic_Feedback_property_react_to" type="{Check,ValueChanged,Script}"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Automatic_Feedback_property_lang_attribute" type="string"/>
	</model>
<css>.ui-dialog &gt; .ui-dialog-titlebar.automatic_feedback_tooltip_title {
    width: 1em;
    padding-top: 0.2em;
    padding-left: 0.4em;
    padding-right: 0.4em;
    padding-bottom: 0.2em;
    position: absolute;
    right: 0.3em;
    z-index: 999;
}

.ui-dialog &gt; .ui-dialog-content.automatic_feedback_tooltip_body {
    padding-right: 2.5em;
}

.automatic_feedback_button {
    width: 1.5em;
    height: 1.5em;
}
</css><view>&lt;div class="automatic_feedback_wrapper"&gt;&lt;/div&gt;
</view><preview>&lt;div class="external-link-button-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonAutomatic_Feedback_create() {
    var presenter = function() {};

    presenter.activityHandler = null;
    presenter.playerController = null;
    presenter.textParser = null;
    presenter.isLoaded = false;

    var ERROR_MESSAGES = {
        AM01: "Activity module ID cannot be empty",
        AT01: "Activity type not implemented",
        F01: "Incorrect Activity item syntax",
    }

    var DISPLAY_MODES = {
        BLOCK: "block",
        TOOLTIPS: "tooltips",
        POPUP: "popup"
    };

    var REACT_TO = {
        VALUE_CHANGED: "ValueChanged",
        CHECK: "Check",
        SCRIPT: "Script"
    }

    var FEEDBACK_CLASSES = {
        CORRECT: "correct-feedback",
        INCORRECT: "incorrect-feedback",
        PARTIAL: "partial-feedback",
        EMPTY: "empty-feedback"
    }

    var POPUP_FEEDBACK_CLASS = "automatic-feedback-popup";
    var AUTOMATIC_FEEDBACK_DIALOG_CLASS = "automatic-feedback-dialog";
    var AUTOMATIC_FEEDBACK_BUTTON_CLASS = "automatic_feedback_button";
    var REMAIN_OPEN_CLASS = "remain-open";

    var activityTypeDict = null;
    presenter.initializeActivityTypeDict = function() {
        activityTypeDict = {
            "Default": DefaultActivity,
        };
    }

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.initialize = function (view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.initializeActivityTypeDict();
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);

        if (!presenter.configuration.isValid) {
            view.innerHTML = ERROR_MESSAGES[presenter.configuration.errorCode];
            return;
        }

        if (!isPreview) {
            presenter.$content = $(view).find('.automatic_feedback_wrapper');
            presenter.tooltipElements = [];
            presenter.activityHandler = new activityTypeDict[presenter.configuration.activityType](this);

            if (presenter.configuration.displayMode == DISPLAY_MODES.POPUP) {
                presenter.$popup = $('&lt;div&gt;&lt;/div&gt;');
                presenter.$content.append(presenter.$popup);
                presenter.$popup.addClass(POPUP_FEEDBACK_CLASS);
                presenter.$popup.dialog({
                    modal: true,
                    autoOpen: false,
                    draggable: false,
                    minHeight: 'auto',
                    resizable: false,
                    width: presenter.configuration.width
                });

                if (presenter.configuration.displayFeedbackButtons) {
                    presenter.activityHandler.createTooltips();
                } else {
                    presenter.$popup.on('dialogclose', function(event, ui){
                        presenter.activityHandler.popupClosedCallback();
                    });
                }
            } else if (presenter.configuration.displayMode == DISPLAY_MODES.TOOLTIPS) {
                presenter.activityHandler.createTooltips();
            }

            MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.$view.get(0));
            MutationObserverService.setObserver();
        }

        presenter.isLoaded = true;
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

       presenter.upgradeLangAttribute(upgradedModel);
       presenter.upgradeResetResponse(upgradedModel);

        return upgradedModel;
    }
    
    presenter.upgradeLangAttribute = function (upgradedModel) {
        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] = "";
        }
    }
    
    presenter.upgradeResetResponse = function (upgradedModel) {
        if (upgradedModel["ResetResponseOnPageChange"] === undefined) {
            upgradedModel["ResetResponseOnPageChange"] = "False";
        }
    }

    presenter.validateModel = function(model) {

        if (model["ActivityModuleID"].trim().length === 0) {
            return {isValid: false, errorCode: "AM01"};
        }

        var activityType = "Default";
        if (model["ActivityType"].length &gt; 0) activityType = model["ActivityType"];
        if(Object.keys(activityTypeDict).indexOf(activityType) === -1) {
            return {isValid: false, errorCode: "AT01"};
        }

        var displayMode = DISPLAY_MODES.BLOCK;
        if (model["Display"].length &gt; 0) displayMode = model["Display"];

        var reactTo = REACT_TO.CHECK;
        if (model["ReactTo"].length &gt; 0) reactTo = model["ReactTo"];


        var feedbacks = generateFeedbacks(model);
        if (!feedbacks.isValid) return {isValid: false, errorCode: feedbacks.errorCode};

        return {
            ID: model.ID,
            isValid: true,
            width: parseInt(model["Width"], 10),
            height: parseInt(model["Height"], 10),
            activityModuleID: model["ActivityModuleID"],
            activityType: activityType,
            resetResponseOnPageChange: ModelValidationUtils.validateBoolean(model["ResetResponseOnPageChange"]),
            displayFeedbackButtons: ModelValidationUtils.validateBoolean(model["DisplayFeedbackButtons"]),
            defaultFeedback: feedbacks.defaultFeedback,
            itemFeedbacks: feedbacks.itemFeedbacks,
            displayMode: displayMode,
            reactTo: reactTo,
            langTag: model["langAttribute"]
        }
    }

    function generateFeedbacks(model) {
        var feedbacks = {
            defaultFeedback: createEmptyFeedbackObject(),
            itemFeedbacks: {},
            isValid: true
        };
        feedbacks = fillFeedbackObjects(feedbacks, 'correct', model['CorrectFeedback']);
        feedbacks = fillFeedbackObjects(feedbacks, 'incorrect', model['IncorrectFeedback']);
        feedbacks = fillFeedbackObjects(feedbacks, 'empty', model['EmptyFeedback']);
        feedbacks = fillFeedbackObjects(feedbacks, 'partial', model['PartialFeedback']);
        return feedbacks;
    }

    function fillFeedbackObjects(feedbacks, fieldName, feedbackModelList) {
        if (!feedbacks.isValid) return feedbacks;

        for (var i = 0; i &lt; feedbackModelList.length; i++) {
            var feedbackModel = feedbackModelList[i];
            if (feedbackModel['Feedback'].trim() === 0) continue;
            if (feedbackModel['ActivityItem'].trim().length === 0) {
                let feedback = feedbackModel['Feedback'];
                if (presenter.textParser) {
                    feedback = presenter.textParser.parse(feedback);
                }
                feedbacks.defaultFeedback[fieldName] = feedback;
            } else {
                activityItems = feedbackModel['ActivityItem'].split(',');
                for (var j = 0; j &lt; activityItems.length; j++) {
                    var activityItem = activityItems[j];
                    var range = activityItem.split('-');
                    if (range.length === 1 || range.length === 2) {
                        var r1, r2;
                        r1 = range[0].trim();
                        if (range.length == 2) {
                            r2 = range[1].trim();
                        } else {
                            r2 = r1;
                        }
                        if (isNaN(r1) || isNaN(r2)) {
                            feedbacks.isValid = false;
                            feedbacks.errorCode = "F01";
                            return feedbacks;
                        }
                        r1 = parseInt(r1);
                        r2 = parseInt(r2) + 1;
                        if (r2 &lt;= r1) {
                            feedbacks.isValid = false;
                            feedbacks.errorCode = "F01";
                            return feedbacks;
                        }
                        for (var k = r1; k &lt; r2; k++) {
                            if (feedbacks.itemFeedbacks[k] === undefined) feedbacks.itemFeedbacks[k] = createEmptyFeedbackObject();
                            let feedback = feedbackModel['Feedback'];
                            if (presenter.textParser) {
                                feedback = presenter.textParser.parse(feedback);
                            }
                            feedbacks.itemFeedbacks[k][fieldName] = feedback;
                        }
                    } else {
                        feedbacks.isValid = false;
                        feedbacks.errorCode = "F01";
                        return feedbacks;
                    }
                }
            }
        }
        return feedbacks;
    }

    function createEmptyFeedbackObject() {
        return {
            correct: "",
            incorrect: "",
            empty: "",
            partial: ""
        }
    }

    presenter.setShowErrorsMode = function () {
        if (presenter.configuration.reactTo == REACT_TO.CHECK) {
            presenter.activityHandler.onShowErrorsMode();
        } else if (presenter.configuration.reactTo == REACT_TO.VALUE_CHANGED) {
            if (presenter.configuration.displayMode !== DISPLAY_MODES.BLOCK || presenter.configuration.displayFeedbackButtons) {
                presenter.activityHandler.clearFeedback();
            }
        }
    }

    presenter.setWorkMode = function () {
        if (presenter.configuration.displayMode !== DISPLAY_MODES.BLOCK
            || presenter.configuration.displayFeedbackButtons
            || presenter.configuration.reactTo === REACT_TO.CHECK) {
            presenter.activityHandler.clearFeedback();
        }
    }

    presenter.reset = function () {
        presenter.activityHandler.clearFeedback();
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;
        eventBus.addEventListener("ValueChanged", this);
    }

    presenter.onEventReceived = function (eventName, data) {
        if (!presenter.isLoaded) return;
        if (eventName == "ValueChanged") presenter.onValueChanged(data);
    }

    presenter.onValueChanged = function(data) {
        if (presenter.configuration.reactTo == REACT_TO.VALUE_CHANGED) {
            if (data.source == presenter.configuration.activityModuleID) {
                if (presenter.configuration.activityType == "Default") {
                    presenter.activityHandler.onShowErrorsMode();
                } else {
                    if (data.value.length === 0 || data.value == '---') {
                        presenter.activityHandler.onEmptyAnswer(data.item);
                    } else if (data.score == 0) {
                        presenter.activityHandler.onIncorrectAnswer(data.item);
                    } else {
                        presenter.activityHandler.onCorrectAnswer(data.item);
                    }
                }
            } else {
                presenter.activityHandler.clearFeedback();
            }
        }
    }

    presenter.showBlockFeedback = function(feedback, _class) {
        presenter.clearFeedbackClasses(presenter.$content);
        presenter.$content.addClass(_class);
        presenter.$content.html(feedback);
        if (presenter.configuration.displayFeedbackButtons) {
            presenter.wrapElementInButton(presenter.$content, false);
        }
    }

    presenter.clearBlockFeedback = function() {
        presenter.clearFeedbackClasses(presenter.$content);
        presenter.$content.html("");
        if (presenter.configuration.displayFeedbackButtons) {
            presenter.$content.parent().find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS).remove();
        }
    }

    presenter.isBlockFeedbackWrapped = function() {
        return presenter.$content.parent().find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS).length &gt; 0;
    }

    presenter.unwrapBlockFeedback = function() {
        if (presenter.configuration.displayFeedbackButtons) {
            presenter.$content.parent().find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS).click();
        }
    }

    presenter.clearFeedbackClasses = function($element) {
        $element.removeClass(FEEDBACK_CLASSES.CORRECT);
        $element.removeClass(FEEDBACK_CLASSES.INCORRECT);
        $element.removeClass(FEEDBACK_CLASSES.PARTIAL);
        $element.removeClass(FEEDBACK_CLASSES.EMPTY);
    }

    presenter.showPopupFeedback = function(feedback, _class) {
        $('.' + POPUP_FEEDBACK_CLASS).dialog("close");
        presenter.clearFeedbackClasses(presenter.$popup.parent());
        presenter.$popup.parent().addClass(_class);
        presenter.$popup.parent().addClass(AUTOMATIC_FEEDBACK_DIALOG_CLASS);
        presenter.$popup.html(feedback);

        var currentScrollTop = presenter.playerController.iframeScroll();
        presenter.$popup.dialog('open');
        var topWindow = window;
        if (!presenter.playerController.isPlayerInCrossDomain()) {
            topWindow = top.window;
            $(topWindow).scrollTop(currentScrollTop);
        }

        var dialogHeight = presenter.$popup.outerHeight();
        var windowHeight = $(topWindow).height();
        var topPosition = parseInt(( windowHeight - dialogHeight) / 3, 10) + currentScrollTop;
        presenter.$popup.parent().css('top',topPosition+'px');

        presenter.scaleDialog(presenter.$popup);
    }

    presenter.scaleDialog = function ($dialog) {
        var scaleInfo = presenter.playerController.getScaleInformation();
        if(scaleInfo.scaleY !== 1.0) {
            var $icpage = presenter.findICPage();
            if ($icpage != null) {
                var presentationPosition = $icpage.offset();
                var presentationWidth = $icpage.outerWidth();
                var dialogWidth = $dialog.outerWidth();
                var presentationHorizontalOffset = parseInt((presentationWidth - dialogWidth) * scaleInfo.scaleY / 2, 10);
                var leftPosition = presentationPosition.left + presentationHorizontalOffset;

                $dialog.parent().css('transform', scaleInfo.transform);
                $dialog.parent().css('transform-origin', scaleInfo.transformOrigin);
                $dialog.parent().css('left',leftPosition+'px');
            }
        }
    }

    presenter.findICPage = function () {
        var $icpage = $(presenter.$view.parent('.ic_page:first')[0]);
        if ($icpage.offset() == null){
            $icpage = $(presenter.$view.parent('.ic_popup_page:first')[0]);
        }
        if ($icpage.offset() == null){
            $icpage = $(presenter.$view.parent('.ic_header:first')[0]);
        }
        if ($icpage.offset() == null){
            $icpage = $(presenter.$view.parent('.ic_footer:first')[0]);
        }
        return $icpage;
    };

    function getFeedbackClassFromElement($element) {
        if ($element.hasClass(FEEDBACK_CLASSES.CORRECT)) {
            return FEEDBACK_CLASSES.CORRECT;
        } else if ($element.hasClass(FEEDBACK_CLASSES.INCORRECT)) {
            return FEEDBACK_CLASSES.INCORRECT;
        } else if ($element.hasClass(FEEDBACK_CLASSES.EMPTY)) {
            return FEEDBACK_CLASSES.EMPTY;
        } else if ($element.hasClass(FEEDBACK_CLASSES.PARTIAL)) {
            return FEEDBACK_CLASSES.PARTIAL;
        } else {
            return '';
        }
    }

    presenter.wrapElementInButton = function($element, isDialog) {
        var $parent = $element.parent();
        if (isDialog) {
            $parent.find('.ui-dialog-titlebar').css('display', 'none');
            if (!$parent.attr('data-original-width')) {
                $parent.attr('data-original-width', $parent.css('width'));
            }
            originalWidth = $element.parent().css('width');
            $parent.css('width', 'auto');
        }
        $element.css('display','none');

        var $buttons = $parent.find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS);

        if($buttons.length == 0) {
            var $button = $('&lt;button&gt;&lt;/button&gt;');
            $button.addClass(AUTOMATIC_FEEDBACK_BUTTON_CLASS);
            var _class = getFeedbackClassFromElement($parent);
            $button.addClass(_class);
            $parent.append($button);
            if (presenter.configuration.displayMode == DISPLAY_MODES.POPUP) {
                $button.click(function(){
                    presenter.showPopupFeedback($element.html(), _class);
                });
            } else {
                $button.click(function(){
                    $element.css('display','');
                    if (isDialog) {
                        $parent.find('.ui-dialog-titlebar').css('display', '');
                        $parent.css('width', $parent.attr('data-original-width'));
                    }
                    $button.remove();
                });
            }
        } else {
            var $button = $buttons.first();
            presenter.clearFeedbackClasses($button);
            var _class = getFeedbackClassFromElement($parent);
            $button.addClass(_class);
        }
    }

    presenter.isScoreCorrect = function(score, maxScore, errorCount) {
        return score == maxScore &amp;&amp; errorCount == 0;
    }

    presenter.isScoreIncorrect = function(score, maxScore, errorCount) {
        return errorCount &gt; 0;
    }

    presenter.isScoreEmpty = function(score, maxScore, errorCount) {
        return score == 0 &amp;&amp; maxScore &gt; 0 &amp;&amp; errorCount == 0;
    }

    presenter.isScorePartial = function(score, maxScore, errorCount) {
        return score != 0 &amp;&amp; score &lt; maxScore &amp;&amp; errorCount == 0;
    }

    presenter.getState = function() {
        var state = presenter.activityHandler.getState();
        return state;
    }

    presenter.setState = function(state) {
        presenter.reset();
        presenter.activityHandler.setState(state);
    }

    presenter.destroy = function AddonAutomatic_Feedback_destroy() {
        presenter.activityHandler.onDestroy();
    }

    presenter.displayFeedback = function (item, type) {
        if (Array.isArray(item) &amp;&amp; item.length == 2 &amp;&amp; type === undefined) {
            type = item[1];
            item = item[0];
        }
        switch (type.toLowerCase()) {
            case "correct":
                presenter.activityHandler.onCorrectAnswer(item);
                break;
            case "incorrect":
                presenter.activityHandler.onIncorrectAnswer(item);
                break;
            case "partial":
                presenter.activityHandler.onPartialAnswer(item);
                break;
            case "empty":
                presenter.activityHandler.onEmptyAnswer(item);
                break;
        }
    }

    presenter.clearFeedback = function() {
        presenter.activityHandler.clearFeedback();
    }

    presenter.getCorrectFeedbacks = function() {
        return getFeedbackValues("correct");
    }

    presenter.getIncorrectFeedbacks = function() {
        return getFeedbackValues("incorrect");
    }

    presenter.getEmptyFeedbacks = function() {
        return getFeedbackValues("empty");
    }

    presenter.getPartiallyCorrectFeedbacks = function() {
        return getFeedbackValues("partial");
    }

    function getFeedbackValues (type) {
        let result = {};
        if (presenter.configuration.defaultFeedback[type].length &gt; 0) {
            result["default"] = presenter.configuration.defaultFeedback[type];
        }
        let itemIDs = Object.keys(presenter.configuration.itemFeedbacks);
        for (let i = 0; i &lt; itemIDs.length; i++) {
            let itemID = itemIDs[i];
            let item = presenter.configuration.itemFeedbacks[itemID];
            if (item[type].length &gt; 0) {
                result[itemID] = item[type];
            }
        }
        return result;
    }

    presenter.executeCommand = function(name, params) {
        var commands = {
            'displayFeedback': presenter.displayFeedback,
            'readFeedback': presenter.readFeedback,
            'displayAndReadFeedback': presenter.displayAndReadFeedback,
            'clearFeedback': presenter.clearFeedback,
            'getCorrectFeedbacks': presenter.getCorrectFeedbacks,
            'getIncorrectFeedbacks': presenter.getIncorrectFeedbacks,
            'getPartiallyCorrectFeedbacks': presenter.getPartiallyCorrectFeedbacks,
            'getEmptyFeedbacks': presenter.getEmptyFeedbacks
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.displayAndReadFeedback = function(item, type) {
        if (Array.isArray(item) &amp;&amp; item.length == 2 &amp;&amp; type === undefined) {
            type = item[1];
            item = item[0];
        }
        presenter.displayFeedback(item, type);
        presenter.readFeedback(item, type);
    }

    presenter.readFeedback = function(item, type) {
        if (Array.isArray(item) &amp;&amp; item.length == 2 &amp;&amp; type === undefined) {
            type = item[1];
            item = item[0];
        }

        if (["correct","incorrect","partial","empty"].indexOf(type) == -1) return;

        var feedback = "";
        if (item != null &amp;&amp; presenter.configuration.itemFeedbacks[item] !== undefined) {
            feedback = presenter.configuration.itemFeedbacks[item][type];
        } else {
            var feedbackObject = this.presenter.configuration.defaultFeedback;
            feedback = feedbackObject[type];
        }
        let $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        $wrapper.html(feedback);
        let speechTexts = window.TTSUtils.getTextVoiceArrayFromElement($wrapper, presenter.configuration.langTag);
        speak(speechTexts);
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts) {
            tts.speak(data);
        }
    }

    class AbstractActivity {

        constructor (presenter) {
            this.presenter = presenter;
        }

        onCorrectAnswer(itemID) {
            throw "onCorrectAnswer must be implemented";
        }

        onIncorrectAnswer(itemID) {
            throw "onIncorrectAnswer must be implemented";
        }

        onEmptyAnswer(itemID) {
            throw "onEmptyAnswer must be implemented";
        }

        onPartialAnswer(itemID) {
            throw "onPartialAnswer must be implemented";
        }

        onShowErrorsMode() {
            throw "onShowErrorsMode must be implemented";
        }

        clearFeedback() {
            throw "clearFeedback must be implemented";
        }

        createTooltips() {
            throw "createTooltips must be implemented";
        }

        getState() {
            throw "getState must be implemented";
        }

        setState(state) {
            throw "setState must be implemented";
        }

        popupClosedCallback() {
            throw "popupClosedCallback must be implemented";
        }

        onDestroy() {
            throw "onDestroy must be implemented";
        }

    }

    class DefaultActivity extends AbstractActivity {

        isActivated = false;
        lastFeedback = "";
        lastClass = "";

        onCorrectAnswer(itemID) {
            var _class = FEEDBACK_CLASSES.CORRECT;
            var feedback = "";
            if (itemID != null &amp;&amp; presenter.configuration.itemFeedbacks[itemID] !== undefined) {
                feedback = presenter.configuration.itemFeedbacks[itemID].correct;
            } else {
                var feedbackObject = this.presenter.configuration.defaultFeedback;
                feedback = feedbackObject.correct;
            }
            this.displayFeedback(feedback, _class);
        }

        onIncorrectAnswer(itemID) {
            var _class = FEEDBACK_CLASSES.INCORRECT;
            var feedback = "";
            if (itemID != null &amp;&amp; presenter.configuration.itemFeedbacks[itemID] !== undefined) {
                feedback = presenter.configuration.itemFeedbacks[itemID].incorrect;
            } else {
                var feedbackObject = this.presenter.configuration.defaultFeedback;
                feedback = feedbackObject.incorrect;
            }
            this.displayFeedback(feedback, _class);
        }

        onEmptyAnswer(itemID) {
            var _class = FEEDBACK_CLASSES.EMPTY;
            var feedback = "";
            if (itemID != null &amp;&amp; presenter.configuration.itemFeedbacks[itemID] !== undefined) {
                feedback = presenter.configuration.itemFeedbacks[itemID].empty;
            } else {
                var feedbackObject = this.presenter.configuration.defaultFeedback;
                feedback = feedbackObject.empty;
            }
            this.displayFeedback(feedback, _class);
        }

        onPartialAnswer(itemID) {
            var _class = FEEDBACK_CLASSES.PARTIAL;
            var feedback = "";
            if (itemID != null &amp;&amp; presenter.configuration.itemFeedbacks[itemID] !== undefined) {
                feedback = presenter.configuration.itemFeedbacks[itemID].partial;
            } else {
                var feedbackObject = this.presenter.configuration.defaultFeedback;
                feedback = feedbackObject.partial;
            }
            this.displayFeedback(feedback, _class);
        }

        onShowErrorsMode() {
            if (!this.presenter.isLoaded || this.presenter.playerController == null) return;
            var module = this.presenter.playerController.getModule(this.presenter.configuration.activityModuleID);
            if (module == null) return;
            if (!module.hasOwnProperty("getScore")
                || !module.hasOwnProperty("getMaxScore")
                || !module.hasOwnProperty("getErrorCount")) return;
            var score = module.getScore();
            var maxScore = module.getMaxScore();
            var errorCount = module.getErrorCount();

            var feedbackObject = this.presenter.configuration.defaultFeedback;
            var feedback = "";
            var _class = "";
            if (this.presenter.isScoreIncorrect(score, maxScore, errorCount)) {
                feedback = feedbackObject.incorrect;
                _class = FEEDBACK_CLASSES.INCORRECT;
            } else if (this.presenter.isScoreCorrect(score, maxScore, errorCount)) {
                feedback = feedbackObject.correct;
                _class = FEEDBACK_CLASSES.CORRECT;
            } else if (this.presenter.isScoreEmpty(score, maxScore, errorCount)) {
                feedback = feedbackObject.empty;
                _class = FEEDBACK_CLASSES.EMPTY;
            } else {
                feedback = feedbackObject.partial;
                _class = FEEDBACK_CLASSES.PARTIAL;
            }
            this.displayFeedback(feedback, _class);
        }

        displayFeedback(feedback, _class) {
            this.isActivated = true;
            this.lastFeedback = feedback;
            this.lastClass = _class;
            if (this.presenter.configuration.displayMode == DISPLAY_MODES.BLOCK) {
                this.presenter.showBlockFeedback(feedback, _class);
            } else if (this.presenter.configuration.displayMode == DISPLAY_MODES.POPUP) {
                if (this.presenter.configuration.displayFeedbackButtons) {
                    if (this.$tooltip == null) {this.createTooltips();};
                    if (this.$tooltip != null) this.showTooltipFeedback(feedback, _class);
                } else {
                    this.presenter.showPopupFeedback(feedback, _class);
                }
            } else {
                if (this.$tooltip == null) {this.createTooltips();};
                if (this.$tooltip != null) this.showTooltipFeedback(feedback, _class);
            }
        }

        clearFeedback() {
            this.isActivated = false;
            const displayMode = this.presenter.configuration.displayMode;

            if (displayMode === DISPLAY_MODES.BLOCK) {
                this.presenter.clearBlockFeedback();
            } else if (displayMode === DISPLAY_MODES.POPUP) {
                this.presenter.$popup.removeClass(REMAIN_OPEN_CLASS);
                this.presenter.$popup.dialog('close');
                if (this.presenter.configuration.displayFeedbackButtons) {
                    this.clearTooltips();
                }
            } else {
                if (this.$tooltip) {
                    this.clearTooltips();
                }
            }
        }

        clearTooltips() {
            if (this.$tooltip) {
                this.$tooltip.removeClass(REMAIN_OPEN_CLASS);
                this.$tooltip.dialog('close');
            }
        }

        getActivityModuleView() {
            var $icpanel = this.presenter.$view.closest('.ic_page_panel');
            if ($icpanel.length &gt; 0) {
                return $icpanel.find('#'+this.presenter.configuration.activityModuleID)[0];
            } else {
                return $('#'+this.presenter.configuration.activityModuleID)[0];
            }
        }

        createTooltips() {
            var activityModuleView = this.getActivityModuleView();
            if (activityModuleView == null) return;
            var $tooltipElement = $('&lt;div&gt;&lt;/div&gt;');
            this.presenter.$content.append($tooltipElement);
            this.$tooltip = $tooltipElement.dialog({
                modal: false,
                autoOpen: false,
                draggable: false,
                minHeight: 'auto',
                resizable: false,
                position: {
                    my: "left center",
                    at: "right center",
                    of: activityModuleView
                }
            });
            var self = this;
            this.$tooltip.on('dialogbeforeclose', function(event, ui){
                if (self.$tooltip.hasClass('remain-open')) {
                    self.presenter.wrapElementInButton(self.$tooltip, true);
                    return false;
                }
                return true;
                });
            if (!this.presenter.configuration.displayFeedbackButtons) {
                this.$tooltip.on('dialogclose', function(event, ui){
                    this.isActivated = false;
                });
            }
            this.$tooltip.addClass('automatic_feedback_tooltip_body');
            this.$tooltip.parent().find('.ui-dialog-titlebar').addClass('automatic_feedback_tooltip_title');
        }

        showTooltipFeedback = function(feedback, _class) {
            this.presenter.clearFeedbackClasses(this.$tooltip.parent());
            this.$tooltip.parent().addClass(_class);
            this.$tooltip.parent().addClass(AUTOMATIC_FEEDBACK_DIALOG_CLASS);
            this.$tooltip.html(feedback);
            this.$tooltip.dialog('open');
            this.presenter.scaleDialog(this.$tooltip);
            if (this.presenter.configuration.displayFeedbackButtons) {
                this.$tooltip.addClass(REMAIN_OPEN_CLASS);
                this.presenter.wrapElementInButton(this.$tooltip, true);
            }
        }

        getState = function () {
            const isResetResponse = this.presenter.configuration.resetResponseOnPageChange;
            if (isResetResponse) this.setDefaultState();

            var state = {
                isActivated: this.isActivated,
                lastFeedback: this.lastFeedback,
                lastClass: this.lastClass,
                isWrapped: this.isWrapped()
            };

            return JSON.stringify(state);
        }

        setDefaultState = function () {
            this.isActivated = false;
            this.lastFeedback = "";
            this.lastClass = "";
        }

        isWrapped = function () {
            if (!this.presenter.configuration.displayFeedbackButtons) return false;
            if (this.presenter.configuration.displayMode === DISPLAY_MODES.BLOCK) {
                return this.presenter.isBlockFeedbackWrapped();
            } else if (this.presenter.configuration.displayMode === DISPLAY_MODES.POPUP) {
                return !this.presenter.$popup.dialog('isOpen');
            } else {
                if (this.$tooltip) {
                    return this.$tooltip.parent().find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS).length &gt; 0;
                } else {
                    return false;
                }
            }
        }


        setState = function (state) {
            setTimeout(() =&gt; {
                // without timeout there are issues with positioning of feedback buttons
                const parsed = JSON.parse(state);
                const displayMode = this.presenter.configuration.displayMode;

                this.isActivated = parsed.isActivated;
                this.lastFeedback = parsed.lastFeedback;
                this.lastClass = parsed.lastClass;

                if (this.isActivated) {
                    this.displayFeedback(parsed.lastFeedback, parsed.lastClass);
                } else {
                    this.clearFeedback();
                }

                if (this.presenter.configuration.displayFeedbackButtons &amp;&amp; !parsed.isWrapped) {
                    if (displayMode === DISPLAY_MODES.BLOCK) {
                        this.presenter.unwrapBlockFeedback();
                    } else if (displayMode === DISPLAY_MODES.POPUP) {
                        return !this.presenter.$popup.dialog('open');
                    } else if (this.$tooltip) {
                        this.$tooltip.parent()
                            .find('.' + AUTOMATIC_FEEDBACK_BUTTON_CLASS)
                            .click();
                    }
                }
            }, 0);
        }

        popupClosedCallback = function () {
             this.isActivated = false;
        }

        onDestroy() {
            this.clearTooltips();
            if (this.$tooltip) {
                this.$tooltip.remove();
            }
        }
    }


    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Basic_Math_Gaps" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Decimal Separator" name="decimalSeparator" nameLabel="Basic_Math_Gaps_property_decimal_separator" type="string"/>
        <property displayName="Gaps Definition" name="gapsDefinition" nameLabel="Basic_Math_Gaps_property_gaps_definition" type="text"/>
        <property displayName="Is Equation" name="isEquation" nameLabel="Basic_Math_Gaps_property_is_equation" type="boolean"/>
        <property displayName="Use numeric keyboard" name="useNumericKeyboard" nameLabel="Basic_Math_Gaps_use_numeric_keyboard" type="boolean"/>
        <property displayName="Is Not Activity" name="isNotActivity" nameLabel="Basic_Math_Gaps_property_is_not_activity" type="boolean"/>
        <property displayName="Is Disabled" name="isDisabled" nameLabel="Basic_Math_Gaps_property_is_disabled" type="boolean"/>
        <property displayName="Gap width" name="gapWidth" nameLabel="Basic_Math_Gaps_property_gap_width" type="string"/>
        <property displayName="Gap Type" name="gapType" nameLabel="Basic_Math_Gaps_property_gap_type" type="{Editable, Draggable}"/>
        <property displayName="User action events" name="userActionEvents" nameLabel="Basic_Math_Gaps_property_user_action_events" type="boolean"/>
        <property displayName="Gap Signs" name="Signs" nameLabel="Basic_Math_Gaps_property_gap_signs" type="list">
            <property displayName="Addition" name="Addition" nameLabel="Basic_Math_Gaps_property_gap_signs_addition" type="string"/>
            <property displayName="Subtraction" name="Subtraction" nameLabel="Basic_Math_Gaps_property_gap_signs_subtraction" type="string"/>
            <property displayName="Division" name="Division" nameLabel="Basic_Math_Gaps_property_gap_signs_division" type="string"/>
            <property displayName="Multiplication" name="Multiplication" nameLabel="Basic_Math_Gaps_property_gap_signs_multiplication" type="string"/>
        </property>
    </model>
<css>
.basic-math-gaps-wrapper .basic-math-gaps-container {
    padding: 10px 0px;
    border: 1px solid transparent;
}

.basic-math-gaps-wrapper .basic-math-gaps-container input,
.basic-math-gaps-wrapper .basic-math-gaps-container .element {
    width: 30px;
    text-align: center;
    margin: 0 2px;
}

.basic-math-gaps-wrapper .basic-math-gaps-container &gt; input,
.basic-math-gaps-wrapper .basic-math-gaps-container &gt; .element,
.basic-math-gaps-wrapper .basic-math-gaps-container &gt; .fraction-container,
.basic-math-gaps-wrapper .basic-math-gaps-container &gt; .draggable-gap {
    float: left;
}

.basic-math-gaps-wrapper .basic-math-gaps-container.hasFractions &gt; input,
.basic-math-gaps-wrapper .basic-math-gaps-container.hasFractions &gt; .element {
    margin-top: 15px;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .element {
    display: inline-block;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .fraction-container {
    display: inline-block;
    text-align: center;
    padding: 0 5px;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .fraction-container .numerator {
    padding: 0 5px;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .fraction-container .denominator {
    border-top: 1px solid #000;
    display: block;
    padding: 2px 5px 0px 5px;
    margin-top: 2px;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .hidden-addition {
    width: 0px;
    height: 0px;
    visibility: hidden;
    display: inline-block;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .correct {
    border-color: #00bb00;
    background-color: #bbffbb;
}

.basic-math-gaps-wrapper .basic-math-gaps-container .wrong {
    border-color: #ff1111;
    background-color: #ffbbbb;
}
.basic-math-gaps-wrapper .basic-math-gaps-container.wrong {
    border: 1px solid #ff1111;
}

.basic-math-gaps-wrapper .basic-math-gaps-container.correct {
    border: 1px solid #00bb00;
}

.basic-math-gaps-wrapper .draggable-gap {
    height: 20px;
    text-align: center;
    cursor: pointer;
}
</css><view>&lt;div class="basic-math-gaps-wrapper"&gt;
    &lt;div class="basic-math-gaps-container clearfix"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="basic-math-gaps-wrapper"&gt;
    &lt;div class="basic-math-gaps-container clearfix"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonBasic_Math_Gaps_create(){

    var presenter = function(){};

    presenter.eventBus = null;
    presenter.playerController = null;
    presenter.gapsContainer = null;
    presenter.widgetsFactory = null;
    presenter.lastDraggedItem = null;

    presenter.setPlayerController = function (controller) {
        this.playerController = controller;
    };

    presenter.setEventBus = function(wrappedEventBus) {
        presenter.eventBus = wrappedEventBus;
    };

    presenter.getErrorObject = function (errorCode) {
        return {
            'isError' : true,
            'errorCode' : errorCode
        };
    };

    presenter.errorCodes = {
        'E01' : 'Left side is not equal to Right side.',
        'E02' : 'A space can NOT be a decimal separator.',
        'E03' : 'Gaps Definition can NOT be blank',
        'E04' : 'Gap width must be positive integer',
        'E05' : 'Sign must be other than =, [, ]',
        'E06' : 'Equation needs \'=\' sign to be a equation'
    };

    presenter.createPreview = function(view, model){
        runLogic(view, model, true);
    };

    presenter.run = function(view, model){
        runLogic(view, model, false);

        presenter.setOnEventListeners();
    };

    presenter.setOnEventListeners = function () {
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('ItemSelected', this);
        presenter.eventBus.addEventListener('ItemConsumed', this);
    };

    presenter.upgradeModel = function (model) {
        var nModel = presenter.upgradeGapType(model);
        nModel = presenter.upgradeNumericKeyboard(nModel);
        return presenter.upgradeUserActionEvents(nModel);
    };

    presenter.upgradeGapType = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.gapType == undefined) {
            upgradedModel["gapType"] = "Editable";
        }

        return upgradedModel;
    };

    presenter.upgradeNumericKeyboard = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.useNumericKeyboard === undefined) {
            upgradedModel["useNumericKeyboard"] = "False";
            }

        return upgradedModel;
    };

    presenter.upgradeUserActionEvents = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (model['userActionEvents'] === undefined) {
            upgradedModel['userActionEvents'] = 'False';
        }

        return upgradedModel;
    };

    function deleteCommands() {
        delete presenter.getState;
        delete presenter.setState;
        delete presenter.getScore;
        delete presenter.getMaxScore;
        delete presenter.disable;
        delete presenter.enable;
    }

    function runLogic(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.configuration = presenter.validateModel(upgradedModel);
        presenter.$view = $(view);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(presenter.$view.find('.basic-math-gaps-container'), presenter.errorCodes, presenter.configuration.errorCode);

            deleteCommands();
            return;
        }

        presenter.gapsContainer = new presenter.GapsContainerObject();
        presenter.widgetsFactory = new presenter.ObjectFactory();
        presenter.valueChangeObserver = new presenter.ValueChangeObserver();

        if (isPreview) {
            presenter.eventBus = function () {};
        }

        presenter.createGaps();
        if (!isPreview) {
            presenter.addFocusOutEventListener();
            presenter._addSendEventHandler();
        }

        presenter.$view.find('input').click(function(e) {
            e.stopPropagation();
        });

        presenter.setWrapperCss();

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
    }

    presenter.createGaps = function () {
        presenter.widgetsFactory.produce(
            presenter.ObjectFactory.PRODUCTION_TYPE.GAP_CONTAINER,
            presenter.configuration.gapsDefinition
        );

        presenter.lastDraggedItem = {};
    };

    presenter.setWrapperCss = function () {
        presenter.$view.find('.basic-math-gaps-wrapper :input').each(function(){
            if (presenter.configuration.gapWidth != '') {
                $(this).css("width", presenter.configuration.gapWidth);
            }
        });
    };

    presenter.addFocusOutEventListener = function () {
        if (presenter.configuration.isDisabled) {
            return;
        }

        presenter._addFocusOutEventListener();
    };

    presenter._addSendEventHandler = function () {
        var inputs = presenter.$view.find('input');
        inputs.on("BMG:send_event", function () {
            var item = presenter.$view.find('input').index( this),
                value = $(this).val().trim(),
                score = (($(this).val().trim() == presenter.configuration.gapsValues[item]) || (presenter.reconvertSign(presenter.configuration.Signs, $(this).val().trim()) == presenter.configuration.gapsValues[item]));

            if (presenter.configuration.isEquation
                &amp;&amp; filterInputs(function(element) { return $(element).val().length &gt; 0; }).length != presenter.$view.find('input').length ) {
                return;
            }

            presenter.sendEvent(item, value, score);
        });
    };

    presenter._addFocusOutEventListener = function () {
        var inputs = presenter.$view.find('input');

        if (!presenter.configuration.userActionsEventsEnabled) {
            inputs.focusout(function () {
                $(this).trigger("BMG:send_event");
            });
        }
    };


    function escapeRegexSpecialCharacters(value) {
        return (value + '').replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&amp;'); // escape regex special characters
    }

    function convertDecimalSeparator(value, from, to) {
        if (from == '.') {
            return value;
        }
        var escaped = escapeRegexSpecialCharacters(from);
        return value.replace(new RegExp(escaped, 'g'), to);
    }

    function getValueOfSingleElement(element, isGap, shouldParse) {
        var getGapValuePattern = /\[(.+)\]/,
            isDigitPattern = /\d+/;

        element = isGap ? getGapValuePattern.exec(element)[1] : element.replace(/[\[\]']+/g, '');

        if (isDigitPattern.test(element) &amp;&amp; shouldParse) {
            return parseInt(element, 10);
        } else {
            return element;
        }
    }

    // this is for situation when user pass for example: 1 1/2,
    // which from math point of view is an addition but the addition sign should NOT be displayed
    function checkIsHiddenAddition(list, index) {
        var isDigitPattern = /\d+/;
        if ( index &gt;= list.length ) {
            return false;
        }

        if ( isDigitPattern.test(list[index]) &amp;&amp; isDigitPattern.test(list[index + 1]) ) {
            return true;
        }

        return false;
    }

    presenter.convertSign = function (signs, value) {
        if (typeof (signs) == "undefined") {
            signs = {Addition: "", Subtraction: "", Division: "", Multiplication: ""};
        }

        if (value === "*" &amp;&amp; signs['Multiplication'] !== "") {
            return signs['Multiplication'];
        }
        else if (value === "/" &amp;&amp; signs['Division'] !== "") {
            return signs['Division'];
        }
        else if (value === "+" &amp;&amp; signs['Addition'] !== "") {
            return signs['Addition'];
        }
        else if (value === "-" &amp;&amp; signs['Subtraction'] !== "") {
            return signs['Subtraction'];
        }
        else {
            return value;
        }
    };

    presenter.reconvertSign = function (signs, value) {
        if (typeof (signs) == "undefined") {
            return value;
        }

        switch(value) {
            case signs['Multiplication']:
                return "*";
            case signs['Division']:
                return "/";
            case signs['Addition']:
                return "+";
            case signs['Subtraction']:
                return "-";
            default:
                return value;
        }
    };

    presenter.removeEmptyStringsFromArray = function (stringArray) {
        var resultArray = [];

        var arrayLen = stringArray.length;
        for(var i = 0; i &lt; arrayLen; i++) {
            if (!ModelValidationUtils.isStringEmpty(stringArray[i])) {
                resultArray.push(stringArray[i]);
            }
        }

        return resultArray;
    };

    presenter.validateGapsDefinition = function(model, isEquation, separator, signs) {
        if (model['gapsDefinition'].length === 0) {
            return presenter.getErrorObject('E03');
        }

        var validatedGapsDefinition = [],
            isGapPattern = /^\[.+\]$/,
            splittedGapsBySpace = model['gapsDefinition'].split(' '),
            isFractionPattern = /\d+\]*\/\[*\d+/,
            leftSide = '',
            rightSide = '',
            isLeft = true,
            gapsValues = [];


        splittedGapsBySpace = presenter.removeEmptyStringsFromArray(splittedGapsBySpace);

        $.each(splittedGapsBySpace, function(i) {
            var valueBeforeConvert = splittedGapsBySpace[i],
                value = presenter.convertSign(signs, valueBeforeConvert),
                isGap = isGapPattern.test(value),
                isFraction = isFractionPattern.test(value);

            var gapType = isGap ? presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP :
                presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP;

            var singleElement = {
                gapType: gapType,
                originalForm: value,
                beforeConvert: getValueOfSingleElement(valueBeforeConvert, isGap, false),
                isGap: isGap,
                isFraction: isFraction,
                parsed: getValueOfSingleElement(value, isGap, true),
                notParsed: getValueOfSingleElement(value, isGap, false),
                isHiddenAdditionAfter: checkIsHiddenAddition(splittedGapsBySpace, i)
            };

            if (isFraction) {
                var numerator = value.split('/')[0],
                    denominator = value.split('/')[1],
                    isNumeratorGap = isGapPattern.test(numerator),
                    isDenominatorGap = isGapPattern.test(denominator);

                if (!isGap) {
                    isNumeratorGap = isGapPattern.test(numerator);
                    isDenominatorGap = isGapPattern.test(denominator);
                } else {
                    isNumeratorGap = true;
                    isDenominatorGap = true;
                    numerator += ']';
                    denominator = '[' + denominator;
                }

                singleElement.fraction = {
                    numerator: {
                        isGap: isNumeratorGap,
                        originalForm: numerator,
                        parsed: getValueOfSingleElement(numerator, isNumeratorGap, true),
                        notParsed: getValueOfSingleElement(numerator, isNumeratorGap, false)
                    },
                    denominator: {
                        isGap: isDenominatorGap,
                        originalForm: denominator,
                        parsed: getValueOfSingleElement(denominator, isDenominatorGap, true),
                        notParsed: getValueOfSingleElement(denominator, isDenominatorGap, false)
                    }
                };

                singleElement.gapType = presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP;

                if(isNumeratorGap) {
                    gapsValues.push(singleElement.fraction.numerator.notParsed);
                }

                if(isDenominatorGap) {
                    gapsValues.push(singleElement.fraction.denominator.notParsed);
                }

            } else if(singleElement.isGap) {
                gapsValues.push(singleElement.notParsed);
            }

            validatedGapsDefinition.push(singleElement);

            if (singleElement.notParsed == '=') {
                isLeft = false;
                return true; // continue in jQuery.each
            }

            if (isLeft) {
                leftSide += singleElement.beforeConvert;
                if (singleElement.isHiddenAdditionAfter) {
                    leftSide += '+';
                }
            } else {
                rightSide += singleElement.beforeConvert;
                if (singleElement.isHiddenAdditionAfter) {
                    rightSide += '+';
                }
            }
        });

        if (isEquation) {
            leftSide = convertDecimalSeparator(leftSide, separator, '.');
            rightSide = convertDecimalSeparator(rightSide, separator, '.');

            try {
                var leftSideEvaluated = eval(leftSide).toFixed(2),
                rightSideEvaluated = eval(rightSide).toFixed(2);
            } catch (_) {
                return presenter.getErrorObject('E06');
            }

            if(leftSideEvaluated != rightSideEvaluated) {
                return presenter.getErrorObject('E01');
            }
        }

        return {
            'isError' : false,
            'allElements' : validatedGapsDefinition,
            'gapsValues' : gapsValues,
            'leftSide' : isEquation &amp;&amp; splittedGapsBySpace.length &gt; 0 ? leftSideEvaluated : leftSide,
            'rightSide' : isEquation &amp;&amp; splittedGapsBySpace.length &gt; 0 ? rightSideEvaluated : rightSide
        }
    };

    presenter.validateDecimalSeparator = function(separator) {
        var spacePattern = /(\s)/;

        if (spacePattern.test(separator)) {
            return presenter.getErrorObject('E02');
        }

        return {
            'isError' : false,
            'value' : separator.length &gt; 0 ? separator : '.'
        };
    };

    presenter.validateGapWidth = function(gapWidth) {
        if (typeof gapWidth == "undefined" || gapWidth == 0) {
            gapWidth = '34';
        }

        if (gapWidth &lt; 0 || isNaN(gapWidth)) {
            return presenter.getErrorObject('E04');
        }

        return {
            'isError' : false,
            'value' : gapWidth
        }
    };

    presenter.validateSigns = function(signs) {
        var availableFields = ["Addition", "Subtraction", "Division", "Multiplication"];

        if (typeof signs == "undefined") {
            signs = [{Addition: "", Subtraction: "", Division: "", Multiplication: ""}];
        }

        var regexp = new RegExp("[\=\\[\\]]");

        for (var i = 0; i &lt; availableFields.length; i++) {
            var field = availableFields[i];
            if (regexp.test(signs[0][field])) {
                return presenter.getErrorObject('E05');
            }
        }

        return {
            'isError' : false,
            'value' : signs[0]
        }
    };

    presenter.validateGapType = function (model) {
        if(model.gapType === "Draggable") {
            return { value: true };
        }

        return { value: false };
    };

    presenter.onDestroy = function () {
        this.$view.off();
        presenter.$view.find('input').off();
    };

    presenter.validateModel = function(model) {

        var validatedIsEquation = ModelValidationUtils.validateBoolean(model['isEquation']),
            validatedIsDisabled = ModelValidationUtils.validateBoolean(model['isDisabled']),
            validatedIsActivity = !(ModelValidationUtils.validateBoolean(model['isNotActivity'])),
            validatedIsVisible = ModelValidationUtils.validateBoolean(model['Is Visible']),
            validatedUseNumericKeyboard = ModelValidationUtils.validateBoolean(model['useNumericKeyboard']),
            validatedUserActionEvents = ModelValidationUtils.validateBoolean(model['userActionEvents']);

        var validatedDecimalSeparator = presenter.validateDecimalSeparator(model['decimalSeparator']);

        if (validatedDecimalSeparator.isError) {
            return validatedDecimalSeparator;
        }

        var validatedGapWidth = presenter.validateGapWidth(model['gapWidth']);

        if (validatedGapWidth.isError) {
            return validatedGapWidth;
        }

        var validatedSigns = presenter.validateSigns(model['Signs']);

        if (validatedSigns.isError) {
            return validatedSigns;
        }

        var validatedGapsDefinition = presenter.validateGapsDefinition(model, validatedIsEquation, validatedDecimalSeparator.value, validatedSigns.value);
        if (validatedGapsDefinition.isError) {
            return validatedGapsDefinition;
        }

        var validatedGapType = presenter.validateGapType(model);

        return {
            'isError' : false,
            'gapsDefinition' : validatedGapsDefinition.allElements,
            'gapsValues' : validatedGapsDefinition.gapsValues,
            'isEquation' : validatedIsEquation,
            'addonID' : model.ID,
            'rightValue' : validatedGapsDefinition.rightSide,
            'leftValue' : validatedGapsDefinition.leftSide,
            'isActivity' : validatedIsActivity,
            'isNotActivity': !validatedIsActivity,
            'isDisabled' : validatedIsDisabled,
            'isDisabledByDefault': validatedIsDisabled,
            'isVisibleByDefault' : validatedIsVisible,
            'isVisible' : validatedIsVisible,
            'decimalSeparator' : validatedDecimalSeparator.value,
            'gapWidth' : validatedGapWidth.value,
            'isDraggable': validatedGapType.value,
            'Signs' : validatedSigns.value,
            'useNumericKeyboard' : validatedUseNumericKeyboard,
            'userActionsEventsEnabled': validatedUserActionEvents
        }
    };

    presenter.isErrorsMode = false;

    presenter.setShowErrorsMode = function () {
        if(!presenter.isErrorsMode) {
            presenter.gapsContainer.check();
            presenter.isErrorsMode = true;
        }
    };

    presenter.setWorkMode = function() {
        if (presenter.isErrorsMode) {
            presenter.gapsContainer.check();
            presenter.isErrorsMode = false;
        }
    };

    presenter.reset = function(){
        presenter.gapsContainer.reset();

        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.configuration.isDisabled = presenter.configuration.isDisabledByDefault;
        presenter.isErrorsMode = false;
    };

    presenter.areValuesInEquation = function (userValuesInGaps, correctGapsValues) {
        correctGapsValues = [].concat(correctGapsValues);
        var len = userValuesInGaps.length;

        for (var i = 0; i &lt; len; i++) {
            if (correctGapsValues.indexOf(userValuesInGaps[i]) === -1) {
                return false;
            } else {
                var index = correctGapsValues.indexOf(userValuesInGaps[i]);
                correctGapsValues.splice(index, 1);
            }
        }

        return true;
    };

    presenter.isEquationCorrect = function (validatedScore) {
        var isCorrect = presenter.isEquationCorrectWrapper(validatedScore);
        var valuesAreInEquation = presenter.areValuesInEquationWrapper();

        return (isCorrect &amp;&amp; valuesAreInEquation);
    };

    function getReconvertedUserExpression () {
        return presenter.reconvertExpression(getUserExpression().split(' '));
    }

    function getConvertedUserGapsValues () {
        var gapsValues = presenter.reconvertExpression(presenter.gapsContainer.getValues());

        return gapsValues.split(' ');
    }

    presenter.areValuesInEquationWrapper = function () {
        return presenter.areValuesInEquation(getConvertedUserGapsValues(), presenter.configuration.gapsValues);
    };


    presenter.isEquationCorrectWrapper = function (validatedScore) {
        return validatedScore.isSameResultOnBothSides
            &amp;&amp; validatedScore.leftEvaluated == presenter.configuration.leftValue
            &amp;&amp; validatedScore.rightEvaluated == presenter.configuration.rightValue;
    };

    presenter.getErrorCount = function(){
        const wasShowAnswersActive = presenter.configuration.isShowAnswersActive;
        if (presenter.configuration.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        const errorCount = _getErrorCount();
        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        return errorCount;
    };

    function _getErrorCount () {
        if (presenter.cantCheck()) {
            return 0;
        }

        var validated = presenter.validateScore();

        if (presenter.configuration.isEquation) {
            if(!presenter.gapsContainer.areAllGapsFilled()){
                return 0;
            }

            return equationGetErrorCount(validated);
        } else {
            return presenter.gapsContainer.getNonEmptyGapsNumber() - validated.validGapsCount;
        }
    }

    function equationGetErrorCount (validated) {
        if (presenter.isEquationCorrect(validated) ) {
            return 0;
        }

        return 1;
    }

    function filterInputs(testFunction) {
        return $.grep(presenter.$view.find('input'), function(element) { return testFunction(element) });
    }

    presenter.getMaxScore = function(){
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        if (presenter.configuration.isEquation) {
            return 1;
        }

        return presenter.gapsContainer.getMaxScore();
    };

    function getUserExpression() {
        function getValueFromChild($gap) {
            var gapID = $gap.attr("id");
            var gapValue = presenter.gapsContainer.getValueByID(gapID);

            return value = gapValue;
        }

        var result = '';

        $.each(presenter.$view.find('.basic-math-gaps-container').children(), function() {
            var elements = [$(this)];

            if (elements[0].hasClass('fraction-container')) {
                var container = elements[0];
                elements = [];
                elements.push(container.find('.numerator').children());
                elements.push($('&lt;span class="element"&gt;/&lt;/span&gt;'));
                elements.push(container.find('.denominator').children());
            }

            $.each(elements, function() {
                var $element = $(this),
                    value = '';

                if ($element.is('input')) {
                    var gapID = $element.attr("id");
                    var gapValue = presenter.gapsContainer.getValueByID(gapID);
                    value = gapValue + ' ';
                } else if ($element.hasClass("draggableContainer")) {
                    var child = $element.find(":first-child");
                    value = getValueFromChild(child) + ' ';
                } else {
                    value = $element.text() + ' ';
                }
                result += convertDecimalSeparator(value, presenter.configuration.decimalSeparator, '.');
            });
        });

        return result.trim();
    }

    presenter.reconvertExpression = function(splittedUserExpression) {
        var convertedSign,
            reconvertedExpression = '';

        $.each(splittedUserExpression, function(i) {
            convertedSign = presenter.reconvertSign(presenter.configuration.Signs, splittedUserExpression[i]);
            reconvertedExpression += convertedSign + " ";
        });

        return reconvertedExpression.trim();
    };

    presenter.getValidGapsCount = function (valuesArray) {
        var isValid = true;
        var validGapsCount = 0;

        valuesArray.forEach(function (value, index) {
            if (presenter.configuration.gapsValues[index] != presenter.reconvertSign(presenter.configuration.Signs, value)) {
                isValid = false;
            } else if (value.length &gt; 0) {
                validGapsCount++;
            }
        });

        return {
            isValid: isValid,
            validGapsCount: validGapsCount
        }
    };

    presenter.getStringReconvertedUserExpression = function () {
        return getReconvertedUserExpression().split(' ').reduce(function (result, element) {
            return result + element;
        }, '');
    };

    presenter.validateScore = function () {
        var validatedGapsCount = presenter.getValidGapsCount(presenter.gapsContainer.getValues());

        var reconvertedExpression = presenter.getStringReconvertedUserExpression();

        var splitted = reconvertedExpression.split('=');

        if (presenter.configuration.isEquation &amp;&amp; splitted.length &gt; 1 &amp;&amp; filterInputs(function(element) { return $(element).val().length == 0; }).length == 0) {
            try {
                var userExpressionLeft = splitted[0];
                var userExpressionRight = splitted[1];
                var leftEvaluated = eval(userExpressionLeft).toFixed(2);
                var rightEvaluated = eval(userExpressionRight).toFixed(2);
                var isSameResultOnBothSides = leftEvaluated == rightEvaluated;
            } catch (_) {
                leftEvaluated = "";
                rightEvaluated = "";
                isSameResultOnBothSides = false;
            }
        }

        return {
            'isValid' : validatedGapsCount.isValid,
            'isSameResultOnBothSides' : isSameResultOnBothSides,
            'leftEvaluated' : leftEvaluated,
            'rightEvaluated' : rightEvaluated,
            'validGapsCount' : validatedGapsCount.validGapsCount
        };
    };

    presenter.getScore = function () {
        const wasShowAnswersActive = presenter.configuration.isShowAnswersActive;
        if (presenter.configuration.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        const score = _getScore();
        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        return score;
    };

    function _getScore() {
        if (presenter.cantCheck()) {
            return 0;
        }

        var validated = presenter.validateScore();

        if (presenter.configuration.isEquation) {
            if(!presenter.gapsContainer.areAllGapsFilled()){
                return 0;
            }

            if (presenter.isEquationCorrect(validated) ) {
                return 1;
            }

            return 0;
        } else {
            return validated.validGapsCount;
        }
    }

    presenter.cantCheck = function () {
        if (presenter.configuration.isNotActivity
        || presenter.configuration.isDisabled
        || presenter.gapsContainer.areAllGapsEmpty()) {
            return true;
        }

        return false;
    };

    presenter.getState = function(){
        const wasShowAnswersActive = presenter.configuration.isShowAnswersActive;
        if (presenter.configuration.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        var state = {
            'values' : presenter.gapsContainer.getValues(),
            'sources': presenter.gapsContainer.getSources(),
            'isVisible' : presenter.configuration.isVisible,
            'isDisabled' : presenter.configuration.isDisabled,
            'droppedElements' : presenter.gapsContainer.getDroppedElements()
        };

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        return JSON.stringify(state);
    };

    presenter.upgradeState = function (state) {
        if (state.sources == undefined) {
             return presenter.upgradeSources(state);
        }

        return state;
    };

    presenter.upgradeSources = function (state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        upgradedState["sources"] = upgradedState.values.map(function () {
            return "";
        });

        return upgradedState;
    };

    presenter.setDisabledInSetState = function (isDisabled) {
        if (presenter.configuration.isDisabledByDefault &amp;&amp; !isDisabled) {
            presenter.gapsContainer.unlock();
        } else if (!presenter.configuration.isDisabledByDefault &amp;&amp; isDisabled) {
            presenter.gapsContainer.lock();
        } else if (presenter.configuration.isDisabledByDefault &amp;&amp; isDisabled) {
            presenter.gapsContainer.lock();
        }

        presenter.configuration.isDisabled = isDisabled;
    };

    presenter.upgradeDroppedElements = function (state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        upgradedState["droppedElements"] = upgradedState.values.map(function () {
            return "";
        });

        return upgradedState;
    };

    presenter.upgradeStateDroppedElements = function (state) {
        if(state.droppedElements == undefined){
            return presenter.upgradeDroppedElements(state);
        }
        return state;
    };

    presenter.setState = function(stateString){
        var state = JSON.parse(stateString);

        var upgradedState = presenter.upgradeState(state);
        upgradedState = presenter.upgradeStateDroppedElements(upgradedState);

        presenter.gapsContainer.setState(upgradedState.values, upgradedState.sources, upgradedState.droppedElements);

        presenter.configuration.isVisible = upgradedState.isVisible;
        presenter.setDisabledInSetState(upgradedState.isDisabled);

        presenter.setVisibility(upgradedState.isVisible);
    };

    presenter.isAttempted = function() {
	if(!presenter.configuration.isActivity || presenter.gapsContainer.areAllGapsFilled()){
		return true;
	} else {
		return false;
	}
    };
    
    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.find('.basic-math-gaps-wrapper').css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.disable = function() {
        presenter.configuration.isDisabled = true;

        presenter.gapsContainer.lock();
    };

    presenter.enable = function() {
        if (presenter.configuration.isDisabled) {
            presenter.gapsContainer.unlock();
            presenter.configuration.isDisabled = false;
        }
    };

    presenter.isAllOK = function() {
        var score = presenter.getScore();
        return score == presenter.getMaxScore() &amp;&amp; score != 0;
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'disable' : presenter.disable,
            'enable' : presenter.enable,
            'isAllOK' : presenter.isAllOK,
            'getView' : presenter.getView,
	    'isAttempted' : presenter.isAttempted
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getView = function() {
        return this.$view;
    };

    presenter.createEventData = function (item, value, score) {
        if (presenter.configuration.isEquation) {
            score = presenter.isAllOK() ? 1 : 0;
            value = '';
            item = 'all';
        } else {
            score = score ? 1 : 0;
            value.toString();
            item = (item + 1).toString();
        }

        return {
            'source': presenter.configuration.addonID,
            'item': item,
            'value': value,
            'score': score
        };
    };

    presenter.sendEvent = function(item, value, score) {
        var eventData = presenter.createEventData(item, value, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);

        if (presenter.isAllOK() &amp;&amp; !presenter.configuration.isEquation) presenter.sendAllOKEvent();
    };

    presenter.sendAllOKEvent = function () {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }

        if (eventName == "ItemSelected") {
            presenter.lastDraggedItem = eventData;
        }

        if (eventName == "ItemConsumed") {
            presenter.lastDraggedItem = {};
        }
    };

    presenter.showAnswers = function () {
        if (presenter.configuration.isActivity) {
            presenter.isErrorsMode = false;
            presenter.gapsContainer.showAnswers();
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.configuration.isShowAnswersActive) {
            return;
        }

        presenter.gapsContainer.hideAnswers();
        presenter.isErrorsMode = false;
    };

    presenter.parseItemValue = function (item) {
    	return item.replace(/\*\*(.*?)\*\*/g, "&lt;b&gt;$1&lt;/b&gt;").replace(/__(.*?)__/g, "&lt;i&gt;$1&lt;/i&gt;").replace(/__(.*?)_/g, "&lt;i&gt;$1_&lt;/i&gt;").replace(/\*\*(.*?)\*/g, "&lt;b&gt;$1*&lt;/b&gt;").replace(/_(.*?)__/g, "_$1").replace(/\*(.*?)\*\*/g, "*$1");
    };

    presenter.GapsContainerObject = function () {
        this._gaps = {};
        this._gapsOrderArray = [];
    };

    presenter.GapsContainerObject.prototype.getGapIndexByID = function (htmlID) {
        return this._gapsOrderArray.indexOf(htmlID);
    };

    presenter.GapsContainerObject.prototype.getValues = function () {
        return this._gapsOrderArray.map(function (element) {
            return this._gaps[element].getValue();
        }, this);
    };

    presenter.GapsContainerObject.prototype.getMaxScore = function () {
        return this._gapsOrderArray.length;
    };

    presenter.GapsContainerObject.prototype.getSources = function () {
        return this._gapsOrderArray.map(function (gapID){
            return this._gaps[gapID].getSource();
        }, this);
    };

    presenter.GapsContainerObject.prototype.getDroppedElements = function () {
        return this._gapsOrderArray.map(function (gapID){
            return this._gaps[gapID].getDroppedElement();
        }, this);
    };

    presenter.GapsContainerObject.prototype.areAllGapsEmpty = function () {
        var reducedValue = this.getValues().reduce(function (previousElement, currentElement) {
            return previousElement + currentElement;
        });

        return (reducedValue === "");
    };

    presenter.GapsContainerObject.prototype.setState = function (valuesArray, sourcesArray, droppedElementsArray) {
        this._gapsOrderArray.forEach(function (gapID, index) {
            if (presenter.configuration.isDraggable) {
                this._gaps[gapID].setState(valuesArray[index], sourcesArray[index], droppedElementsArray[index]);
            } else {
                this._gaps[gapID].value = valuesArray[index];
                this._gaps[gapID].source = sourcesArray[index];
                this._gaps[gapID].$view.val(valuesArray[index]);
            }

            if (valuesArray[index] == "") {
                this._gaps[gapID].destroyDraggableProperty();
            }else{
                this.addGapFilled(gapID);
            }

        }, this);
    };

    presenter.GapsContainerObject.prototype.addGapFilled = function (gapID){
        this._gaps[gapID].removeCssClass("gapEmpty");
        this._gaps[gapID].addCssClass("gapFilled");
        this._gaps[gapID].notifyEdit();
    };

    presenter.GapsContainerObject.prototype.showAnswers = function () {
        this._gapsOrderArray.forEach(function (gapID) {
            this._gaps[gapID].showAnswers();
        }, this);
    };

    presenter.GapsContainerObject.prototype.hideAnswers = function () {
        this._gapsOrderArray.forEach(function (gapID) {
            this._gaps[gapID].hideAnswers();
        }, this);
    };

    presenter.GapsContainerObject.prototype.canSendEvent = function () {
        if (presenter.configuration.isEquation) {
            return this.getNonEmptyGapsNumber() == this._gapsOrderArray.length;
        }

        return true;
    };

    presenter.GapsContainerObject.prototype.check = function (isSetShow) {
        this._gapsOrderArray.forEach(function (gapID) {
            this._gaps[gapID].check(isSetShow);
        }, this);
    };

    presenter.GapsContainerObject.prototype.lock = function () {
        this._gapsOrderArray.forEach(function (element) {
            this._gaps[element].lock();
        }, this);
    };

    presenter.GapsContainerObject.prototype.unlock = function () {
        this._gapsOrderArray.forEach(function (element) {
            this._gaps[element].unlock();
        }, this);
    };

    presenter.GapsContainerObject.prototype.reset = function () {
        this._gapsOrderArray.forEach(function (gapID) {
            this._gaps[gapID].reset();
            this._gaps[gapID].removeCssClass("gapFilled");
            this._gaps[gapID].removeCssClass("gapEmpty");
        }, this);
    };

    presenter.GapsContainerObject.prototype.getNonEmptyGapsNumber = function () {
        return this.getValues().filter(function (value) {
            return value.length &gt; 0;
        }).length;
    };

    presenter.GapsContainerObject.prototype.areAllGapsFilled = function () {
        return (this.getNonEmptyGapsNumber() == this._gapsOrderArray.length);
    };

    presenter.GapsContainerObject.prototype.addGap = function (gap) {
        var gapID = gap.getObjectID();

        this._gapsOrderArray.push(gapID);

        this._gaps[gapID] = gap;
    };

    presenter.GapsContainerObject.prototype.getValueByID = function (gapID) {
        return this._gaps[gapID].getValue();
    };

    presenter.getCSSConfiguration = function () {
        if (presenter.configuration.isEquation) {
            return {
                showAnswers: 'bmg_show-answers'
            };
        } else {
            return {
                correct: "correct",
                wrong: "wrong",
                showAnswers: 'bmg_show-answers'
            };
        }
    }

    presenter.GapUtils = function (configuration) {
        DraggableDroppableObject.call(this, configuration, presenter.getCSSConfiguration());

        this.valueChangeObserver = presenter.valueChangeObserver;
    };

    presenter.GapUtils.prototype = Object.create(DraggableDroppableObject.prototype);
    presenter.GapUtils.constructor = presenter.GapUtils;

    presenter.GapUtils.prototype.addClassToContainer = function () {
        presenter.$view.find('.basic-math-gaps-container').addClass("basic_math_gaps_check");
    };

    presenter.GapUtils.prototype.removeClassFromContainer = function () {
        presenter.$view.find('.basic-math-gaps-container').removeClass("basic_math_gaps_check");
    };

    presenter.GapUtils.prototype.removeClassInEquation = function () {
        presenter.$view.find('.basic-math-gaps-container').removeClass('correct wrong');
    };

    presenter.GapUtils.prototype.getClassName = function () {
        if (presenter.isEquationCorrect(presenter.validateScore())) {
            return "correct";
        } else {
            return "wrong";
        }
    };

    presenter.GapUtils.prototype.addClassInEquation = function () {
        if (presenter.gapsContainer.areAllGapsFilled()) {
            presenter.$view.find('.basic-math-gaps-container').addClass(this.getClassName());
        }
    };

    presenter.GapUtils.prototype.shouldNotAddCssClassInEquation = function (valueFunction) {
        return presenter.configuration.isNotActivity
        || presenter.configuration.isDisabled
        || presenter.gapsContainer.areAllGapsEmpty();
    };
    
    presenter.GapUtils.prototype.shouldNotAddCssClass = function () {
        return presenter.configuration.isNotActivity
        || presenter.configuration.isDisabled
        || this.isEmpty();
    };

    presenter.GapUtils.prototype.setCss = function (containerCssFunction, equationFunction, notEquationFunction) {
        containerCssFunction();

        if (presenter.configuration.isEquation) {
            if (this.shouldNotAddCssClassInEquation()) {
                return;
            }
            equationFunction.call(this);
        } else {
            if (this.shouldNotAddCssClass()) {
                return;
            }
            notEquationFunction.call(this);
        }
    };

    presenter.GapUtils.prototype.isEmpty = function () {
        return this.getValue() == "";
    }

    presenter.GapUtils.prototype.onBlock = function () {
        if (!presenter.configuration.isDisabled) {
            this.lock();
            if(this.isEmpty()){
                this.addCssClass("gapEmpty");
            }
        }
    };

    presenter.GapUtils.prototype.onUnblock = function () {
        if (!presenter.configuration.isDisabled) {
            this.unlock();
            this.removeCssClass("gapEmpty");
        }
    };

    presenter.GapUtils.prototype.onShowAnswers = function () {
        presenter.configuration.isShowAnswersActive = true;

        if (presenter.configuration.isDisabled) {
            this.setViewValue(this.showAnswersValue);
        } else {
            DraggableDroppableObject.prototype.onShowAnswers.call(this);
        }
    };

    presenter.GapUtils.prototype.onHideAnswers = function () {
        presenter.configuration.isShowAnswersActive = false;

        if (presenter.configuration.isDisabled) {
            this.setViewValue(this.value);
        } else {
            DraggableDroppableObject.prototype.onHideAnswers.call(this);
        }
    };

    presenter.GapUtils.prototype.onReset = function () {
        DraggableDroppableObject.prototype.onReset.call(this);

        if (presenter.configuration.isDisabledByDefault &amp;&amp; !presenter.configuration.isDisabled) {
            this.lock();
        } else if (!presenter.configuration.isDisabledByDefault &amp;&amp; presenter.configuration.isDisabled) {
            this.unlock();
        }
    };

    presenter.GapUtils.prototype.onCorrect = function () {
        this.onBlock();
    };

    presenter.GapUtils.prototype.onUnCorrect = function () {
        this.onUnblock();
    };

    presenter.GapUtils.prototype.onWrong = function () {
        this.onBlock();
    };

    presenter.GapUtils.prototype.onUnWrong = function () {
        this.onUnblock();
    };

    presenter.GapUtils.prototype.setCssOnCorrect = function () {
        this.setCss(this.addClassToContainer, this.addClassInEquation, DraggableDroppableObject.prototype.setCssOnCorrect);
    };

    presenter.GapUtils.prototype.setCssOnUnCorrect = function () {
        this.setCss(this.removeClassFromContainer, this.removeClassInEquation, DraggableDroppableObject.prototype.setCssOnUnCorrect);
    };

    presenter.GapUtils.prototype.setCssOnWrong = function () {
        this.setCss(this.addClassToContainer, this.addClassInEquation, DraggableDroppableObject.prototype.setCssOnWrong);
    };

    presenter.GapUtils.prototype.setCssOnUnWrong = function () {
        this.setCss(this.removeClassFromContainer, this.removeClassInEquation, DraggableDroppableObject.prototype.setCssOnUnWrong);
    };

    presenter.GapUtils.prototype.isCorrect = function () {
        if (presenter.configuration.isEquation) {
            return presenter.isEquationCorrect(presenter.validateScore());
        } else {
            return DraggableDroppableObject.prototype.isCorrect.call(this);
        }
    };


    presenter.EditableInputGap = function (htmlID, correctAnswer) {
        var configuration = {
            addonID: presenter.configuration.addonID,
            objectID: htmlID,
            eventBus: presenter.eventBus,
            getSelectedItem: presenter.getSelectedItem,

            showAnswersValue: correctAnswer,

            createView: presenter.EditableInputGap.prototype.createView,
            connectEvents: presenter.EditableInputGap.prototype.connectEvents,
            setViewValue: presenter.EditableInputGap.prototype.setViewValue
        };

        presenter.GapUtils.call(this, configuration);
    };

    presenter.EditableInputGap.prototype = Object.create(presenter.GapUtils.prototype);
    presenter.EditableInputGap.constructor = presenter.EditableInputGap;

    presenter.EditableInputGap.prototype.connectEvents = function () {
        this.$view.on("input", this.onEdit.bind(this));
        this.$view.on('keyup', this.onKeyUp.bind(this));
        this.$view.on("keypress", this.onKeyPress.bind(this));
        this.$view.off('change').bind('change', this.onEdit.bind(this));
    };

    presenter.EditableInputGap.prototype.createView = function () {
        var inputType = "text";
        if (presenter.configuration.useNumericKeyboard) {
            inputType = "tel";
        }
        var $inputGap = $('&lt;input type="' + inputType + '" value="" id="' + this.objectID + '" /&gt;');
        $inputGap.css({
            width: presenter.configuration.gapWidth + "px"
        });
        if ((presenter.configuration.useNumericKeyboard)) {
            $inputGap.attr("step", "any");
        }
        $inputGap.attr("autocomplete", "off");
        return $inputGap;
    };

    presenter.EditableInputGap.prototype.onKeyUp = function(event) {
        event.stopPropagation();
        if (presenter.configuration.useNumericKeyboard) {
            var newText = String(event.target.value);
            var pattern = StringUtils.getNumericPattern();
            if (newText.length &gt; 0 &amp;&amp; !newText.match(pattern)) {
                var patternWithoutLastCharacter = pattern.slice(0, -1);
                var regExp = RegExp(patternWithoutLastCharacter);
                var match = regExp.exec(newText);

                if (match) {
                    this.setViewValue(match[0]);
                } else {
                    this.setViewValue("");
                }
            }
        }
    };

    presenter.EditableInputGap.prototype.onKeyPress = function(event) {
        event.stopPropagation();
        if (presenter.configuration.useNumericKeyboard) {
            var key = String.fromCharCode(!event.charCode ? event.which : event.charCode);
            var selectionStartIdx = event.target.selectionStart;
            var selectionEndIdx = event.target.selectionEnd;
            var oldText = String(event.target.value);
            var newText = oldText.substring(0, selectionStartIdx)
                            + key
                            + oldText.substring(selectionEndIdx);
            var pattern = StringUtils.getNumericPattern();
            if (!newText.match(pattern)) {
                event.preventDefault();
            }
        }
    };

    presenter.EditableInputGap.prototype.onEdit = function (event) {
        this.notifyEdit();
        this.value = this.getValue();

        if(presenter.configuration.isDisabled) {
            return;
        }

        if (presenter.configuration.userActionsEventsEnabled) {
            presenter.$view.find("#" + this.getObjectID()).trigger("BMG:send_event");
        }
    };

    presenter.EditableInputGap.prototype.lock = function () {
        this.$view.attr('disabled','disabled');
        this.$view.addClass('disabled');
    };

    presenter.EditableInputGap.prototype.unlock = function () {
        this.$view.removeAttr('disabled');
        this.$view.removeClass('disabled');
    };

    presenter.EditableInputGap.prototype.getValue = function () {
        return this.$view.val().trim();
    };

    presenter.EditableInputGap.prototype.setViewValue = function (value) {
        return this.$view.val(value);
    };

    presenter.EditableInputGap.prototype.getSource = function () {
        return "";
    };

    presenter.EditableInputGap.prototype.onShowAnswers = function () {
        this.value = this.getValue();
        presenter.GapUtils.prototype.onShowAnswers.call(this);
    };

    presenter.DraggableDroppableGap = function (htmlID, correctAnswer) {
        var configuration = {
            addonID: presenter.configuration.addonID,
            objectID: htmlID,
            eventBus: presenter.eventBus,
            getSelectedItem: presenter.getSelectedItem,

            showAnswersValue: correctAnswer,

            fillGap: presenter.DraggableDroppableGap.prototype.fillGap,
            makeGapEmpty: presenter.DraggableDroppableGap.prototype.makeGapEmpty
        };


        presenter.GapUtils.call(this, configuration);

        this.$view.css({
            width: presenter.configuration.gapWidth + "px",
            display: 'inline-block'
        });

        this.addCssClass("draggable-gap");
    };

    presenter.DraggableDroppableGap.prototype = Object.create(presenter.GapUtils.prototype);
    presenter.DraggableDroppableGap.parent = presenter.GapUtils.prototype;
    presenter.DraggableDroppableGap.constructor = presenter.DraggableDroppableGap;


    presenter.DraggableDroppableGap.prototype.fillGap = function (selectedItem) {
        DraggableDroppableObject.prototype.fillGap.call(this, selectedItem);
        this.notify();
        this.addCssClass("gapFilled");
        this.removeCssClass("gapEmpty")
    };

    presenter.DraggableDroppableGap.prototype.makeGapEmpty = function () {
        DraggableDroppableObject.prototype.makeGapEmpty.call(this);
        this.notify();
        this.removeCssClass("gapFilled");
    };

    presenter.DraggableDroppableGap.prototype.notify = function () {
        this.valueChangeObserver.notify(this.getValueChangeEventData());
    };

    presenter.DraggableDroppableGap.prototype.getValueChangeEventData = function () {
        return {
            htmlID: this.getObjectID(),
            value: this.getValue(),
            isCorrect: this.getValue() == this.showAnswersValue
        };
    };

    presenter.ElementGapObject = function (value) {
        this._value = value;
        this.$view = this._createView();
    };


    presenter.ElementGapObject.prototype._createView = function () {
        return $('&lt;span class="element"&gt;' + this._value + '&lt;/span&gt;');
    };

    presenter.ElementGapObject.prototype.getView = function () {
        return $('&lt;span class="element"&gt;' + this._value + '&lt;/span&gt;');
    };

    presenter.getSelectedItem = function () {
        var item = presenter.lastDraggedItem;

        presenter.onEventReceived("ItemSelected", {});

        return item;
    };


    presenter.FractionGapObject = function (id) {
        this._id = id;
        this._$mainContainer;
        this._$numeratorContainer;
        this._$denominatorContainer;
        this._numerator;
        this._denominator;
        this._numeratorValue;
        this._denominatorValue;

        this._createContainers();
    };

    presenter.FractionGapObject.prototype.constructor = presenter.FractionGapObject;

    presenter.FractionGapObject.prototype._createContainers = function () {
        this._$mainContainer = $('&lt;span class="fraction-container"&gt;&lt;/span&gt;');
        this._$numeratorContainer = $('&lt;span class="numerator"&gt;&lt;/span&gt;');
        this._$denominatorContainer = $('&lt;span class="denominator"&gt;&lt;/span&gt;');
    };

    presenter.FractionGapObject.prototype.createElementNumerator = function (value) {
        this._numerator = presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP, value);

        this._numeratorValue = value;
    };

    presenter.FractionGapObject.prototype.createElementDenominator = function (value) {
        this._denominator = presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP, value);

        this._denominatorValue = value;
    };

    presenter.FractionGapObject.prototype.setDisplayInlineBlock = function (gap) {
        gap.getView().css({
            display: "inline-block"
        });
    };

    presenter.FractionGapObject.prototype.createGapNumerator = function (type, data) {
         this._numerator = presenter.widgetsFactory.produce(type, this._getNominatorProductionData(data));
    };

    presenter.FractionGapObject.prototype.createGapDenominator = function (type, data) {
         this._denominator = presenter.widgetsFactory.produce(type, this._getDenominatorProductionData(data));
    };

    presenter.FractionGapObject.prototype._getNominatorProductionData = function (data) {
        return {
            id: this._id + "-numerator",
            width: presenter.configuration.gapWidth,
            correctAnswer: data.correctAnswerNumerator
        };
    };

    presenter.FractionGapObject.prototype._getDenominatorProductionData = function (data) {
        return {
            id: this._id + "-denominator",
            width: presenter.configuration.gapWidth,
            correctAnswer: data.correctAnswerDenominator
        };
    };

    presenter.FractionGapObject.prototype.getView = function () {
        return this._$mainContainer;
    };

    presenter.FractionGapObject.prototype.joinNumeratorDenominator = function () {
        this._$numeratorContainer.append(this._numerator.getView());
        this._$denominatorContainer.append(this._denominator.getView());

        this._$mainContainer.append(this._$numeratorContainer).append(this._$denominatorContainer);
    };

    presenter.FractionGapObject.prototype.getNumerator = function () {
        return this._numerator;
    };

    presenter.FractionGapObject.prototype.getDenominator = function () {
        return this._denominator;
    };

    presenter.ObjectFactory = function () {
        this.gapsFactory = new presenter.GapsFactoryObject();
        this.gapsContainerFactory = new presenter.GapsContainerFactoryObject();
    };

    presenter.ObjectFactory.PRODUCTION_TYPE = {
        EDITABLE_INPUT_GAP: 0,
        FRACTION_GAP: 1,
        ELEMENT_GAP: 2,
        DRAGGABLE_MATH_GAP: 3,
        GAP_CONTAINER: 4
    };

    presenter.ObjectFactory.prototype.produce = function (type, data) {
        var producedItem;

        switch (type) {
            case presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP:
                producedItem = this.gapsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP, data);
                break;
            case presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP:
                producedItem = this.gapsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP, data);
                break;
            case presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP:
                producedItem = this.gapsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP, data);
                break;
            case presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP:
                producedItem = this.gapsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP, data);
                break;
            case presenter.ObjectFactory.PRODUCTION_TYPE.GAP_CONTAINER:
                producedItem = this.gapsContainerFactory.produce(data);
                break;
        }

        return producedItem;
    };

    presenter.FractionBuilderObject = function () {};

    presenter.FractionBuilderObject.prototype = Object.create(presenter.ObjectFactory.prototype);
    presenter.FractionBuilderObject.prototype.constructor = presenter.FractionBuilderObject;

    presenter.FractionBuilderObject.prototype.produce = function (data) {
        var fraction = this._produceFractionObject(data);

        this._setNumerator(fraction, data);
        this._setDenominator(fraction, data);


        fraction.setDisplayInlineBlock(fraction.getNumerator());
        fraction.setDisplayInlineBlock(fraction.getDenominator());

        fraction.joinNumeratorDenominator();

        return fraction;
    };

    presenter.FractionBuilderObject.prototype._produceFractionObject = function (data) {
        return new presenter.FractionGapObject(data.fractionID);
    };

    presenter.FractionBuilderObject.prototype._setNumerator = function (fraction, data) {
        if (data.fraction.numerator.isGap) {
            if (presenter.configuration.isDraggable) {
                fraction.createGapNumerator(presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP, data);
            } else {
                fraction.createGapNumerator(presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP, data);}

        } else {
            fraction.createElementNumerator(data.fraction.numerator.parsed);
        }
    };

    presenter.FractionBuilderObject.prototype._setDenominator = function (fraction, data) {
        if (data.fraction.denominator.isGap) {
            if (presenter.configuration.isDraggable) {
                fraction.createGapDenominator(presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP, data);
            } else {
                fraction.createGapDenominator(presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP, data);
            }
        } else {
            fraction.createElementDenominator(data.fraction.denominator.parsed);
        }
    };


    presenter.GapsFactoryObject = function () {
        this._fractionBuilder = new presenter.FractionBuilderObject();
    };

    presenter.GapsFactoryObject.prototype = Object.create(presenter.ObjectFactory.prototype);
    presenter.GapsFactoryObject.prototype.constructor = presenter.GapsFactoryObject;

    presenter.GapsFactoryObject.prototype.produce = function (type, data) {
        switch (type) {
            case presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP:
                return this.produceEditableInputGap(data);
                break;
            case presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP:
                return this.produceDraggableMathGap(data);
            case presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP:
                return this.produceElementGap(data);
            case presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP:
                return this.produceFractionGap(data);
                break;
        }
    };

    presenter.GapsFactoryObject.prototype.produceEditableInputGap = function (data) {
        return new presenter.EditableInputGap(data.id, data.correctAnswer);
    };

    presenter.GapsFactoryObject.prototype.produceDraggableMathGap = function (data) {
        return new presenter.DraggableDroppableGap(data.id, data.correctAnswer);
    };

    presenter.GapsFactoryObject.prototype.produceElementGap = function (value) {
        return new presenter.ElementGapObject(value);
    };

    presenter.GapsFactoryObject.prototype.produceFractionGap = function (data) {
        return this._fractionBuilder.produce(data);
    };

    presenter.GapsContainerFactoryObject = function () {};

    presenter.GapsContainerFactoryObject.prototype = Object.create(presenter.ObjectFactory.prototype);
    presenter.GapsContainerFactoryObject.prototype.constructor = presenter.GapsContainerFactoryObject;

    presenter.GapsContainerFactoryObject.prototype.getFractionIndexIncrement = function (dataElement) {
        var increment = 0;

        if (dataElement.fraction.denominator.isGap) {
            increment++;
        }

        if (dataElement.fraction.numerator.isGap) {
            increment++;
        }

        return increment;
    };

    function wrapItemInContainer(item) {
        var $container = $('&lt;div&gt;&lt;/div&gt;');
        $container.css({
            height: "50px",
            'text-align': 'center',
            float: 'left',
            display: 'inline-block'
        });
        $container.addClass("draggableContainer");
        $container.append(item.getView());

        return $container
    }

    function setFractionCssToMainContainer($mainContainer) {
        $mainContainer.addClass('hasFractions');
        $mainContainer.find(".draggableContainer").css({
            'margin-top': "15px"
        });
    }

    presenter.GapsContainerFactoryObject.prototype.produce = function (data) {
        var container = presenter.$view.find('.basic-math-gaps-container');
        var hasFractions = false;


        var dataIndex;
        var elementIndex;
        var draggable = false;
        for(dataIndex = 0, elementIndex = 0; dataIndex &lt; data.length; dataIndex += 1) {
            var dataElement = data[dataIndex];
            var item;

            switch (dataElement.gapType) {
                case presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP:
                    item = this._produceGap(this._getGapData(elementIndex));
                    elementIndex++;
                    presenter.gapsContainer.addGap(item);
                    draggable = true;
                    break;
                case presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP:
                    item = this._produceElementGap(dataElement);
                    break;
                case presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP:
                    item = this._produceFractionGap(dataElement, this._getElementId(elementIndex), elementIndex);
                    elementIndex += this.getFractionIndexIncrement(dataElement);
                    this._addFractionGap(item, dataElement);
                    hasFractions = true;
                    break;
            }


            if (draggable) {
                var $wrappedItem = wrapItemInContainer(item);
                container.append($wrappedItem);
                draggable = false;
            } else {
                container.append(item.getView());
            }

            if (dataElement.isHiddenAdditionAfter) {
                container.append($('&lt;span class="hidden-addition"&gt;+&lt;/span&gt;'));
            }
        }

        if (presenter.configuration.isDisabledByDefault) {
            presenter.gapsContainer.lock();
        }

        if (hasFractions) {
            setFractionCssToMainContainer(container);
        }
    };

    presenter.GapsContainerFactoryObject.prototype._addFractionGap = function (fractionGap, gapDefinition) {

        if (gapDefinition.fraction.numerator.isGap) {
            presenter.gapsContainer.addGap(fractionGap.getNumerator());
        }

        if (gapDefinition.fraction.denominator.isGap) {
            presenter.gapsContainer.addGap(fractionGap.getDenominator());
        }
    };

    presenter.GapsContainerFactoryObject.prototype._produceFractionGap = function (data, id, elementIndex) {
        data.fractionID = id;

        if (data.fraction.numerator.isGap) {
            data["correctAnswerNumerator"] = presenter.convertSign(presenter.configuration.Signs, presenter.configuration.gapsValues[elementIndex]);
            elementIndex++;
        }

        if (data.fraction.denominator.isGap) {
            data["correctAnswerDenominator"] = presenter.convertSign(presenter.configuration.Signs, presenter.configuration.gapsValues[elementIndex]);
        }

        return presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.FRACTION_GAP, data);
    };

    presenter.GapsContainerFactoryObject.prototype._produceElementGap = function (data) {
        return presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.ELEMENT_GAP, data.originalForm);
    };


    presenter.GapsContainerFactoryObject.prototype._produceGap = function (data) {
        var gap;

        if (presenter.configuration.isDraggable) {
            gap = presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.DRAGGABLE_MATH_GAP, data);
        } else {
            gap = presenter.widgetsFactory.produce(presenter.ObjectFactory.PRODUCTION_TYPE.EDITABLE_INPUT_GAP, data);
        }

        return gap;
    };

    presenter.GapsContainerFactoryObject.prototype._getGapData = function (index) {
        return {
            id: this._getElementId(index),
            width: presenter.configuration.gapWidth,
            value: "",
            source: "",
            correctAnswer: presenter.convertSign(presenter.configuration.Signs, presenter.configuration.gapsValues[index])
        };
    };

    presenter.GapsContainerFactoryObject.prototype._getElementId = function (index) {
        return (presenter.configuration.addonID + "-" + index);
    };

    presenter.ValueChangeObserver = function () {};

    presenter.ValueChangeObserver.prototype.notify = function (data) {
        var eventData = presenter.createEventData(presenter.gapsContainer.getGapIndexByID(data.htmlID), data.value, data.isCorrect);

        if (!presenter.gapsContainer.canSendEvent()) {
            return;
        }

        presenter.eventBus.sendEvent('ValueChanged', eventData);
        if (presenter.isAllOK() &amp;&amp; !presenter.configuration.isEquation) presenter.sendAllOKEvent();
    };

    return presenter;
}

AddonBasic_Math_Gaps_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="BlocklyCodeEditor" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="sceneID" nameLabel="BlocklyCodeEditor_property_scene_id" type="string"/>
		<property displayName="Toolbox" name="toolbox" nameLabel="BlocklyCodeEditor_property_toolbox" type="list">
			<property name="blockName" nameLabel="BlocklyCodeEditor_property_block_name" type="editableselect">
				<property name="colour_rgb" nameLabel="Blockly_translation_RGB_color" type="text"/>
				<property name="text_append" nameLabel="Blockly_translation_append_text" type="text"/>
				<property name="math_arithmetic" nameLabel="Blockly_translation_arithmetic" type="text"/>
				<property name="math_single" nameLabel="Blockly_translation_basic_functions" type="text"/>
				<property name="colour_blend" nameLabel="Blockly_translation_blend_color" type="text"/>
				<property name="logic_boolean" nameLabel="Blockly_translation_boolean" type="text"/>
				<property name="controls_flow_statements" nameLabel="Blockly_translation_break" type="text"/>
				<property name="math_change" nameLabel="Blockly_translation_change_variable" type="text"/>
				<property name="text_charAt" nameLabel="Blockly_translation_char_at" type="text"/>
				<property name="colour_picker" nameLabel="Blockly_translation_color_picker" type="text"/>
				<property name="logic_compare" nameLabel="Blockly_translation_compare" type="text"/>
				<property name="math_constant" nameLabel="Blockly_translation_constants" type="text"/>
				<property name="math_constrain" nameLabel="Blockly_translation_constrain" type="text"/>
				<property name="lists_split" nameLabel="Blockly_translation_create_list_from" type="text"/>
				<property name="lists_repeat" nameLabel="Blockly_translation_create_list_with_item" type="text"/>
				<property name="lists_create_with" nameLabel="Blockly_translation_create_list" type="text"/>
				<property name="controls_forEach" nameLabel="Blockly_translation_for_each" type="text"/>
				<property name="controls_for" nameLabel="Blockly_translation_for" type="text"/>
				<property name="lists_getIndex" nameLabel="Blockly_translation_get_list_element" type="text"/>
				<property name="lists_getSublist" nameLabel="Blockly_translation_get_sublist" type="text"/>
				<property name="variables_get" nameLabel="Blockly_translation_get_variable" type="text"/>
				<property name="controls_if" nameLabel="Blockly_translation_if" type="text"/>
				<property name="lists_isEmpty" nameLabel="Blockly_translation_is_empty_list" type="text"/>
				<property name="text_isEmpty" nameLabel="Blockly_translation_is_empty_text" type="text"/>
				<property name="text_join" nameLabel="Blockly_translation_join_text" type="text"/>
				<property name="lists_indexOf" nameLabel="Blockly_translation_list_index_of" type="text"/>
				<property name="lists_length" nameLabel="Blockly_translation_list_length" type="text"/>
				<property name="logic_operation" nameLabel="Blockly_translation_logic_operation" type="text"/>
				<property name="logic_ternary" nameLabel="Blockly_translation_logic_ternary" type="text"/>
				<property name="math_on_list" nameLabel="Blockly_translation_math_list_functions" type="text"/>
				<property name="math_modulo" nameLabel="Blockly_translation_modulo" type="text"/>
				<property name="logic_negate" nameLabel="Blockly_translation_negate" type="text"/>
				<property name="logic_null" nameLabel="Blockly_translation_null" type="text"/>
				<property name="math_number_property" nameLabel="Blockly_translation_number_property" type="text"/>
				<property name="math_number" nameLabel="Blockly_translation_number" type="text"/>
				<property name="text_print" nameLabel="Blockly_translation_print" type="text"/>
				<property name="text_prompt_ext" nameLabel="Blockly_translation_prompt_for" type="text"/>
				<property name="colour_random" nameLabel="Blockly_translation_random_color" type="text"/>
				<property name="math_random_float" nameLabel="Blockly_translation_random_fraction" type="text"/>
				<property name="math_random_int" nameLabel="Blockly_translation_random_integer" type="text"/>
				<property name="controls_whileUntil" nameLabel="Blockly_translation_repeat_while/until" type="text"/>
				<property name="controls_repeat_ext" nameLabel="Blockly_translation_repeat" type="text"/>
				<property name="math_round" nameLabel="Blockly_translation_round" type="text"/>
				<property name="lists_setIndex" nameLabel="Blockly_translation_set_list_element" type="text"/>
				<property name="variables_set" nameLabel="Blockly_translation_set_variable" type="text"/>
				<property name="lists_sort" nameLabel="Blockly_translation_sort_list" type="text"/>
				<property name="text_getSubstring" nameLabel="Blockly_translation_substring" type="text"/>
				<property name="text_indexOf" nameLabel="Blockly_translation_text_index_of" type="text"/>
				<property name="text_length" nameLabel="Blockly_translation_text_length" type="text"/>
				<property name="text" nameLabel="Blockly_translation_text" type="text"/>
				<property name="text_changeCase" nameLabel="Blockly_translation_to_upper/lower_case" type="text"/>
				<property name="math_trig" nameLabel="Blockly_translation_trigonometry" type="text"/>
				<property name="text_trim" nameLabel="Blockly_translation_trim" type="text"/>
				<property name="variables_editor" nameLabel="Blockly_translation_variables_editor" type="text"/>
				<property name="grid_scene_commands" nameLabel="Blockly_translation_scene_commands" type="text"/>
				<property name="custom" nameLabel="Blockly_translation_custom" type="string"/>
			</property>
			<property displayName="Category" name="blockCategory" nameLabel="BlocklyCodeEditor_property_block_category" type="string"/>
			<property displayName="Is Disabled" name="blockIsDisabled" nameLabel="BlocklyCodeEditor_property_block_is_disabled" type="boolean"/>
		</property>
		<property displayName="Custom Blocks" name="customBlocks" nameLabel="BlocklyCodeEditor_property_custom_blocks" type="list">
			<property displayName="Name" name="customBlockName" nameLabel="BlocklyCodeEditor_property_custom_block_name" type="string"/>
			<property displayName="Title" name="customBlockTitle" nameLabel="BlocklyCodeEditor_property_custom_block_title" type="string"/>
			<property displayName="Color" name="customBlockColor" nameLabel="BlocklyCodeEditor_property_custom_block_color" type="string"/>
			<property displayName="Inputs" name="customBlockInputs" nameLabel="BlocklyCodeEditor_property_custom_block_inputs" type="string"/>
			<property displayName="Inputs Type" name="customBlockInputsType" nameLabel="BlocklyCodeEditor_property_custom_block_inputs_type" type="string"/>
			<property displayName="Inputs Text" name="customBlockInputsText" nameLabel="BlocklyCodeEditor_property_custom_block_inputs_text" type="string"/>			<property displayName="Code" name="customBlockCode" nameLabel="BlocklyCodeEditor_property_custom_block_code" type="text"/>
			<property displayName="Connection" name="customBlockOutput" nameLabel="BlocklyCodeEditor_property_custom_block_output" type="{None,Left,Top,Bottom,Top-Bottom}"/>
			<property displayName="Connection Type" name="customBlockOutputType" nameLabel="BlocklyCodeEditor_property_custom_block_output_type" type="string"/>
		</property>
		<property displayName="Block limit" name="maxBlocksLimit" nameLabel="BlocklyCodeEditor_property_max_block_limit" type="string"/>
		<property displayName="Hide Run" name="hideRun" nameLabel="BlocklyCodeEditor_property_hide_run" type="boolean"/>
		<property name="blocksTranslation" nameLabel="BlocklyCodeEditor_property_blocksTranslation" type="text"/>
		<property name="initialConfiguration" nameLabel="BlocklyCodeEditor_property_initial_configuration" type="text"/>
	</model>
<css>.blocklyEditor-wrapper button {
    margin-top: 10px;
}

.blockly-cover {
    top : 0;
    position : absolute;
    width : 100%;
    height : 100%;
    background-color: gray;
    opacity: .5;
}
</css><view>&lt;div class="blocklyEditor-wrapper"&gt;
    &lt;div class="blockly-code-editor-editor"&gt;&lt;/div&gt;
    &lt;button class="blockly-code-editor-run"&gt;Run&lt;/button&gt;
&lt;/div&gt;

</view><preview>&lt;div class="blocklyEditor-wrapper"&gt;
    &lt;div class="blockly-code-editor-editor"&gt;&lt;/div&gt;
    &lt;button class="blockly-code-editor-run"&gt;Run&lt;/button&gt;
&lt;/div&gt;
</preview><presenter>/**
 * KNOWN ISSUES:
 *      Toolbox Menu
 *          - when header is on page, the toolbox is moved by header height and there is need to change it top property,
 *          issue due to blockly library have lack of setting toolbox under parent HTML element, instead it places on page
 *
 *          - updatingToolbox do not update toolbox menu element ".blocklyFlyout", after updating toolbox there is need to update
 *          transform translate to width of ".blocklyToolboxDiv"
 */
function AddonBlocklyCodeEditor_create () {
    var presenter = function () {};

    presenter.ERROR_CODES = {
        "SI01": "Scene id must have value",
        "SI02": "You must add scene id if you want to add scene toolbox",
        "CN01": "Color must be integer positive value in range 0..360",
        "CE01": "Addon can't have blocks with category and without category in the same time",
        "DN01": "Custom block name must be unique",
        "IE01": "Undefined input or connection type",
        "OE01": "Undefined connection type",
        "DE01": "Duplicated elements in toolbox",
        "BL01": "Block limit must be  0 or positive integer number",
        "BL02": "Block limit is float number but should be integer value.",
        "VN01": "Input name must be a valid JS variable name",
        "UB01": "Undefined block in toolbox",
        "TP01": "Translation in Toolbox is not a valid JSON.",
        "TP02": "Translation is not a valid JSON",
        "VE01": "Variable editor must have category name"
    };

    presenter.inputsType = {
        "ANY" : "Any",
        "BOOLEAN" : "Boolean",
        "NUMBER" : "Number",
        "STRING" : "String",
        "ARRAY" : "Array"
    };
    presenter.connections = {
        "NONE" : function Blockly_NONE_Connection_function () {return ""; },
        "LEFT" : function Blockly_LEFT_Connection_function (type) {return "this.setOutput(true,'" + type + "');"; },
        "TOP" :  function Blockly_TOP_Connection_function (type) {return "this.setPreviousStatement(true,'" + type + "');";},
        "BOTTOM" : function Blockly_BOTTOM_Connection_function (type) {return "this.setNextStatement(true,'" + type + "');";},
        "TOP-BOTTOM" : function Blockly_TOP_BOTTOM_Connection_function (firstType, secondType) { return presenter.connections["TOP"](firstType) + presenter.connections["BOTTOM"](secondType); }
    };

    presenter.DEFAULT_BLOCKS_TRANSLATION_LABELS = [
         "controls_if",
         "logic_compare",
         "logic_operation",
         "logic_negate",
         "logic_boolean",
         "logic_null",
         "logic_ternary",
         "controls_repeat_ext",
         "controls_whileUntil",
         "controls_for",
         "controls_forEach",
         "controls_flow_statements",
         "math_number",
         "math_arithmetic",
         "math_single",
         "math_trig",
         "math_constant",
         "math_number_property",
         "math_round",
         "math_on_list",
         "math_modulo",
         "math_constrain",
         "math_random_int",
         "math_random_float",
         "text",
         "text_join",
         "text_append",
         "text_length",
         "text_isEmpty",
         "text_indexOf",
         "text_charAt",
         "text_getSubstring",
         "text_changeCase",
         "text_trim",
         "text_print",
         "text_prompt_ext",
         "lists_create_with",
         "lists_repeat",
         "lists_length",
         "lists_getIndex",
         "lists_isEmpty",
         "lists_indexOf",
         "lists_setIndex",
         "lists_getSublist",
         "lists_split",
         "lists_sort",
         "colour_picker",
         "colour_random",
         "colour_rgb",
         "colour_blend",
         "math_change",
         "variables_set",
         "variables_get",
         "variables_editor"
    ];

    presenter.javaScriptBlocksCode = [];
    presenter.blockDefinitions = [];

    presenter.configuration = {
        hideRun: null,
        sceneID: null,
        sceneModule: null,
        workspace: null,
        toolboxXML: "",
        addSceneToolbox: false,
        sceneToolboxName: "",
        sceneToolboxIsCategory: false,
        isPreview: false,
        isValid: false,
        haveSceneID: true,
        isVisible: true,
        visibleByDefault: true,
        pageLoaded: false,
        blocksTranslation: {}
    };

    presenter.savedRawState = '';

    function isPreviewDecorator(func) {
        if (!presenter.configuration.isPreview) {
            return func;
        } else {
            return function Blockly_empty_preview_decorator_function () {};
        }
    }

    presenter.run = function Blockly_run_function (view, model) {
        presenter.runLogic(view, model, false);
    };

    presenter.createPreview = function Blockly_createPreview_function (view, model) {
        presenter.runLogic(view, model, true);
    };

    presenter.setPlayerController = function Blockly_setPlayerController_function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this, true);
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.runLogic = function Blockly_runLogic_function (view, model, isPreview) {
        presenter.configuration.isPreview = isPreview;
        presenter.configuration = $.extend(presenter.configuration, presenter.validateModel(model));

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.view = view;
        presenter.$view = $(view);

        presenter.setRunButton();
        presenter.setEditorCss();
        presenter.setVisibility(presenter.configuration.visibleByDefault || isPreview); //fallback in case loading the workspace takes a while

        setTimeout(() =&gt; {
            // The creation of the blockly workspace needs to be delayed in case the initial layout is not the default one
            // otherwise the initialization of workspace may be interrupted before completion, leading to errors
            if (view.parentElement == null) return;

            presenter.addUserBlocks();
            presenter.createWorkspace(view, isPreview);

            presenter.setVisibility(presenter.configuration.visibleByDefault || isPreview);

            isPreviewDecorator(presenter.connectHandlers)();

            isPreviewDecorator(presenter.updateToolbox)();

            isPreviewDecorator(presenter.setConfiguration)(presenter.configuration.initialConfiguration);

            if (!isPreview) {
                MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
                MutationObserverService.setObserver();
            }

            if (isPreview) {
                presenter.$view.css('z-index', '0');
            }

            if (presenter.savedRawState.length &gt; 0) {
                presenter.setState(presenter.savedRawState);
                presenter.savedRawState = '';
            }
        });
    };

    presenter.setConfiguration = function(configuration) {
        if(configuration) {
            var xml = Blockly.Xml.textToDom(configuration);
            Blockly.Xml.domToWorkspace(xml, presenter.configuration.workspace);
        }
    };

    presenter.getConfiguration = function() {
        var xml = Blockly.Xml.workspaceToDom(presenter.configuration.workspace);
        return Blockly.Xml.domToText(xml);
    };

    presenter.createWorkspace = function (view, isPreview) {
        var editor = $(view).find(".blockly-code-editor-editor")[0];
        var toolboxXML = "";
        if (isPreview) {
            toolboxXML = presenter.getPreviewToolbox();
        } else {
            toolboxXML = presenter.getToolboxXML();
        }

        presenter.configuration.workspace = Blockly.inject(editor, {
            toolbox: toolboxXML,
            sounds: false,
            maxBlocks: presenter.configuration.maxBlocks
        });
    };

    presenter.setEditorCss = function () {
        presenter.$view.find(".blockly-code-editor-editor").css({
            width: presenter.$view.width(),
            height: presenter.$view.height()
        });
    };

    presenter.addUserBlocks = function () {
        eval(presenter.configuration.customBlocksXML.code);
        eval(presenter.configuration.customBlocksXML.blockDefinitions);
    };

    presenter.addBlocksTranslations = function Blockly_addGoogleBlocksTranslations_function () {
        for (var key in presenter.configuration.blocksTranslation) {
            if (presenter.configuration.blocksTranslation.hasOwnProperty(key)) {
                Blockly.Msg[StringUtils.format('{0}', key)] = presenter.configuration.blocksTranslation[key];
            }
        }
    };

    presenter.destroy = function Blockly_destroy_function (event) {
        if (event.target !== presenter.view) { return; }

        var key, i;
        if (presenter.configuration.isPreview) {
            $("#content").off("scroll", presenter.scrollFixHandler);
        }

        presenter.configuration.workspace.dispose();

        presenter.configuration.workspace = null;

        presenter.$view.find(".blockly-code-editor-run").off();
        presenter.$view = null;
        presenter.view = null;
        presenter.configuration = null;

        for (i = 0; i &lt; presenter.javaScriptBlocksCode.length; i++) {
            delete Blockly.JavaScript[presenter.javaScriptBlocksCode[i]];
        }

        for (i = 0; i &lt; presenter.blockDefinitions.length; i++) {
            delete Blockly.Blocks[presenter.blockDefinitions[i]];
        }

        presenter.javaScriptBlocksCode = null;
        presenter.blockDefinitions = null;

        for (key in presenter.connections) {
            if (presenter.connections.hasOwnProperty(key)) {
                presenter.connections[key] = null;
            }
        }

        presenter.connections = null;
        presenter.removePresenterFunctions();
    };

    presenter.removePresenterFunctions = function Blockly_removePresenterFunctions_function() {
        presenter.removePresenterFunctions = null;
        presenter.destroy = null;
        presenter.ERROR_CODES = null;
        presenter.inputsType = null;
        presenter.connections = null;
        presenter.DEFAULT_BLOCKS_TRANSLATION_LABELS = null;
        presenter.run = null;
        presenter.scrollToolboxPreviewFix = null;
        presenter.scrollFixHandler = null;
        presenter.createPreview = null;
        presenter.setPlayerController = null;
        presenter.runLogic = null;
        presenter.addGoogleBlocksTranslations = null;
        presenter.validateModel = null;
        presenter.validateBlockLimit = null;
        presenter.validateBlocksTranslation = null;
        presenter.validateToolboxNamesWithCustomBlocks = null;
        presenter.validateSceneId = null;
        presenter.validateToolbox = null;
        presenter.validateTranslations = null;
        presenter.validateInputsType = null;
        presenter.validateConnection = null;
        presenter.validateBlock = null;
        presenter.convertCustomBlockToJS = null;
        presenter.convertCustomBlocksToJS = null;
        presenter.validateCustomBlocks = null;
        presenter.connectHandlers = null;
        presenter.setRunButton = null;
        presenter.executeCommand = null;
        presenter.hide = null;
        presenter.show = null;
        presenter.setVisibility = null;
        presenter.getWorkspaceCode = null;
        presenter.getToolboxXML = null;
        presenter.onEventReceived = null;
        presenter.addCustomBlocks = null;
        presenter.getSceneModuleOnPageLoaded = null;
        presenter.updateToolbox = null;
        presenter.getState = null;
        presenter.setState= null;
        presenter.setState = null;
        presenter.setShowErrorsMode = null;
        presenter.setWorkMode = null;
        presenter.showAnswers = null;
        presenter.hideAnswers = null;
        presenter.coverAddon = null;
        presenter.removeAddonCover = null;
        presenter.coverToolbox = null;
        presenter.removeToolboxCover = null;
    };

    presenter.validateIfToolboxHaveOnlyCategoryOrNot = function (validatedToolbox) {
        var isCategory = false;
        var isWithoutCategory = false;
        for (var key in validatedToolbox.value.categories) {
            if (validatedToolbox.value.categories.hasOwnProperty(key)) {
                var blocks = validatedToolbox.value.categories[key];
                for (var i = 0; i &lt; blocks.length; i++) {
                    if (blocks[i].isCategory) {
                        isCategory = true;
                    } else {
                        isWithoutCategory = true;
                    }
                }
            }

            if (isCategory &amp;&amp; isWithoutCategory) {
                break;
            }
        }

        if (presenter.configuration.addSceneToolbox) {
            if (presenter.configuration.sceneToolboxIsCategory) {
                isCategory = true;
            } else {
                isWithoutCategory = true;
            }
        }

        return {
            isError: isCategory === isWithoutCategory,
            errorCode: "CE01"
        };
    };
    presenter.validateModel = function Blockly_validateModel_function(model) {
        var haveSceneID = true;
        var validatedIsVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);

        var validatedToolbox = presenter.validateToolbox(model['toolbox']);
        if (!validatedToolbox.isValid) {
            return validatedToolbox;
        }

        var validatedSceneId = presenter.validateSceneId(model["sceneID"]);
        if (!validatedSceneId.isValid) {
            haveSceneID = false;
        }

        var validatedCustomBlocks = presenter.validateCustomBlocks(model["customBlocks"]);
        if (!validatedCustomBlocks.isValid) {
            return validatedCustomBlocks;
        }

        var validatedBlockLimit = presenter.validateBlockLimit(model['maxBlocksLimit']);
        if (!validatedBlockLimit.isValid) {
            return validatedBlockLimit;
        }

        var validatedToolboxNamesWithCustomBlocks = presenter.validateToolboxNamesWithCustomBlocks(validatedToolbox.value.categories, validatedCustomBlocks);
        if (!validatedToolboxNamesWithCustomBlocks.isValid) {
            return validatedToolboxNamesWithCustomBlocks;
        }

        var validatedBlocksTranslations = presenter.validateBlocksTranslation(model['blocksTranslation']);
        if (!validatedBlocksTranslations.isValid) {
            return validatedBlocksTranslations;
        }

        var validatedMixedCategories = presenter.validateIfToolboxHaveOnlyCategoryOrNot(validatedToolbox);
        if (validatedMixedCategories.isError) {
            return validatedMixedCategories;
        }

        return {
            isValid: true,
            addonID: model['ID'],
            visibleByDefault: validatedIsVisible,
            haveSceneID: haveSceneID,
            hideRun: ModelValidationUtils.validateBoolean(model["hideRun"]),
            sceneID: validatedSceneId.value,
            toolboxXML: validatedToolbox.value.categories,
            customBlocksXML: presenter.convertCustomBlocksToJS(validatedCustomBlocks.value),
            maxBlocks: validatedBlockLimit.value,
            blocksTranslation: $.extend(validatedToolbox.value.translations, validatedBlocksTranslations.value),
            initialConfiguration: model["initialConfiguration"]
        };
    };

    presenter.validateBlockLimit = function Blockly_validateBlockLimit_function(blockModel) {
        if (ModelValidationUtils.isStringEmpty(blockModel.trim())) {
            blockModel = "0";
        }

        var validatedBlockLimit = ModelValidationUtils.validateInteger(blockModel);
        if (!validatedBlockLimit.isValid || validatedBlockLimit.value &lt; 0) {
            return {
                isValid: false,
                errorCode: "BL01"
            };
        }

        /*
            Find all characters without numbers
         */
        if (blockModel.trim().match(/[^0-9]/) !== null) {
             return {
                isValid: false,
                errorCode: "BL02"
            };
        }
        return validatedBlockLimit;
    };

    presenter.validateBlocksTranslation = function Blockly_validateBlocksTranslation_function (blocksTranslations) {
        if (ModelValidationUtils.isStringEmpty(blocksTranslations.trim())){
            return {
                isValid: true,
                value: {}
            }
        }

        var parsedValue = {};
        try {
            parsedValue = JSON.parse(blocksTranslations);
        } catch (error) {
            return {
                isValid: false,
                errorCode: "TP02"
            }
        }

        return {
            isValid: true,
            value : parsedValue
        };
    };

    presenter.validateToolboxNamesWithCustomBlocks = function Blockly_validateToolboxNamesWithCustomBlocks_function (toolbox, customblocks) {
        for (var key in toolbox) {
            if (toolbox.hasOwnProperty(key)) {
                var category = toolbox[key];
                for (var blockInCategoryIndex = 0; blockInCategoryIndex &lt; category.length; blockInCategoryIndex++) {
                    if (presenter.DEFAULT_BLOCKS_TRANSLATION_LABELS.indexOf(category[blockInCategoryIndex].name)  == -1) {
                        var founded = false;
                        for (var customBlockIndex = 0; customBlockIndex &lt; customblocks.value.length; customBlockIndex++) {
                            if (customblocks.value[customBlockIndex].name == category[blockInCategoryIndex].name) {
                                founded = true;
                                break;
                            }
                        }
                        if (!founded) {
                            return {
                                isValid: false,
                                errorCode: "UB01"
                            }
                        }
                    }
                }
            }
        }

        return {
            isValid: true
        }
    };

    presenter.validateSceneId = function Blockly_Code_Editor_validate_scene_id (sceneId) {
        if (ModelValidationUtils.isStringEmpty(sceneId)) {
            return {
                isValid: false,
                errorCode: "SI01"
            };
        }

        return {
            isValid: true,
            value: sceneId
        };
    };

    presenter.validateToolbox = function Blockly_Code_Editor_validate_toolbox (toolbox) {
        var categories = {};
        var elements = [];
        var translations = {};

        for (var index = 0; index &lt; toolbox.length; index++) {
            var toolboxElement = toolbox[index];
            if (toolboxElement['blockName'].name != "custom") {
                if (elements.indexOf(toolboxElement['blockName'].name) != -1) {
                    return {
                        isValid: false,
                        errorCode: "DE01"
                    };
                }
            }
            elements.push(toolboxElement['blockName'].name);

            var validatedTranslations = presenter.validateTranslations(toolboxElement);
            if (!validatedTranslations.isValid) {
                return validatedTranslations;
            }

            $.extend(translations, validatedTranslations.value);
            var validatedCategory = null;
            if (toolboxElement['blockName'].name == "custom") {
                validatedCategory = addToCategory(categories, toolboxElement['blockCategory'], toolboxElement['blockName'].value, toolboxElement['blockIsDisabled']);
            } else if (toolboxElement['blockName'].name == "grid_scene_commands") {
                validatedCategory = addToCategory(categories, toolboxElement['blockCategory'], "grid_scene_commands", toolboxElement['blockIsDisabled']);
            } else {
                validatedCategory = addToCategory(categories, toolboxElement['blockCategory'], toolboxElement['blockName'].name, toolboxElement['blockIsDisabled']);
            }
            if (!validatedCategory.isValid) {
                return validatedCategory;
            }
        }

        return {
            isValid: true,
            value: {
                categories: categories,
                translations: translations
            }
        };
    };



    function addToCategory(categories, categoryName, blockName, isDisabled) {
        if (blockName == '') {
            return {
                isValid: true
            };
        }

        if (ModelValidationUtils.validateBoolean(isDisabled)) {
            return {
                isValid: true
            };
        }

        var haveCategory = true;
        if (categoryName.trim() === '') {
            haveCategory = false;
            categoryName = "Empty";
        }

        if (blockName === "variables_editor" &amp;&amp; !haveCategory) {
            return {
                isValid: false,
                errorCode: "VE01"
            };
        }

        if (categories[categoryName] == null) {
            categories[categoryName] = [];
        }

        if (blockName != "grid_scene_commands") {
            categories[categoryName].push({
                name: blockName,
                isCategory: haveCategory
            });
        } else {
            presenter.configuration.addSceneToolbox = true;
            presenter.configuration.sceneToolboxIsCategory = haveCategory;
            presenter.configuration.sceneToolboxName = categoryName;
        }
        return {
            isValid: true
        };
    }

    presenter.validateTranslations = function Blockly_validateTranslations_function (toolboxElement) {
        if (presenter.DEFAULT_BLOCKS_TRANSLATION_LABELS.indexOf(toolboxElement['blockName'].name) != -1 || (toolboxElement['blockName'].name == "grid_scene_commands")) {
            var parsedValue = "";
            try {
                parsedValue = JSON.parse(toolboxElement['blockName'].value);
            } catch (error) {
                return {
                    isValid: false,
                    errorCode: "TP01"
                };
            }
            return {
                isValid: true,
                value: parsedValue
            }
        }

        return {
            isValid: true,
            value: {}
        }
    };

    presenter.validateType = function Blockly_validateType_function (type) {
        if (type.toUpperCase().trim() in presenter.inputsType) {
            return {
                isValid: true,
                value: presenter.inputsType[type.toUpperCase().trim()]
            };
        }
        return {
            isValid: false,
            errorCode: "IE01"
        };
    };

    presenter.validateInputsType = function Blockly_validateInputsType_function (inputsType, inputsLength) {
        if (inputsLength == 0) {
            return {
                isValid:true,
                value : []
            };
        }

        var validatedInputTypes = [];
        var separatedInputsType = [];
        if (!ModelValidationUtils.isStringEmpty(inputsType.trim())) {
            separatedInputsType = inputsType.split(",");
        }
        for (var actualPosition = separatedInputsType.length; actualPosition &lt; inputsLength.length; actualPosition++) {
            separatedInputsType.push("Any");
        }

        for (var key in separatedInputsType) {
            if (separatedInputsType.hasOwnProperty(key)) {
                var validatedType = presenter.validateType(separatedInputsType[key]);
                if (!validatedType.isValid) {
                    return validatedType;
                }
                validatedInputTypes.push(validatedType.value);
            }
        }

        return {
            isValid: true,
            value: validatedInputTypes
        };
    };

    presenter.validateConnection = function Blockly_validateConnection_function (connection) {
        if (connection.toUpperCase().trim() in presenter.connections) {
            return {
                isValid: true,
                value: connection.toUpperCase().trim()
            };
        }
        return {
            isValid: false,
            errorCode: "OE01"
        };
    };


    presenter.validateBlock = function Blockly_validateBlock_function (customBlock) {
        var validatedColor =  ModelValidationUtils.validateIntegerInRange(customBlock['customBlockColor'], 360, 0);
        if (!validatedColor.isValid) {
            return {
                isValid: false,
                errorCode: "CN01"
            };
        }

        var inputs = customBlock['customBlockInputs'].split(",");
        if (ModelValidationUtils.isStringEmpty(customBlock['customBlockInputs'])) {
            inputs = [];
        }
        for (var i = 0; i &lt; inputs.length; i++) {
            if (!ModelValidationUtils.validateJSVariableName(inputs[i].trim()).isValid) {
                return {
                    isValid: false,
                    errorCode: "VN01"
                };
            }
        }

        var validatedInputsType = presenter.validateInputsType(customBlock['customBlockInputsType'], inputs.length);
        if (!validatedInputsType.isValid) {
            return validatedInputsType;
        }

        var inputsText = customBlock['customBlockInputsText'].split(",");
        for (var actualPosition = inputsText.length; actualPosition &lt; inputs.length; actualPosition++) {
            inputsText.push("");
        }
        var validatedConnection = presenter.validateConnection(customBlock['customBlockOutput']);
        if (!validatedConnection.isValid) {
            return validatedConnection;
        }

        var validatedConnectionsType = presenter.validateInputsType(customBlock['customBlockOutputType'], 2);
        if (!validatedConnectionsType.isValid) {
            return validatedConnectionsType;
        }

        var isTitle = !ModelValidationUtils.isStringEmpty(customBlock['customBlockTitle']);
        var validatedTitle = {
            isValid: true,
            value: null
        };

        if (isTitle) {
            validatedTitle.value = customBlock['customBlockTitle'];
        }

        return {
            isValid: true,
            name:  customBlock['customBlockName'],
            color: validatedColor.value,
            inputs: inputs,
            inputsText: inputsText,
            inputsType: validatedInputsType.value,
            code: customBlock['customBlockCode'].replace(/\r?\n|\r/g,""),   //Removing all new line sings
            connection: validatedConnection.value,
            connectionType: validatedConnectionsType.value,
            title: validatedTitle.value

        };
    };


    presenter.convertCustomBlockToJS = function Blockly_convertCustomBlockToJS_function (customBlock) {
        var blockDefinitions = presenter.createBlockDefinition(customBlock);
        var code = presenter.createCode(customBlock);

        presenter.blockDefinitions.push(customBlock.name);
        presenter.javaScriptBlocksCode.push(customBlock.name);

        return {
            blockDefinitions: blockDefinitions,
            code: code
        };
    };

    presenter.createCode = function Blockly_createCode_function (customBlock) {
        var variables = presenter.getCustomBlockVariablesCode(customBlock);

        var code = StringUtils.format("Blockly.JavaScript['{0}'] = function(block) {", customBlock.name);
        code = presenter.extractInputsValuesFromBlock(customBlock, code);
        code = presenter.addVariablesToCode(customBlock, code, variables);
        code += "return code;};";

        return code;
    };

    presenter.getCustomBlockVariablesCode = function (customBlock) {
        var variables = "";
        for (var inputIndex = 0; inputIndex &lt; customBlock.inputs.length; inputIndex++) {
            variables += StringUtils.format("'var {0} = ' + {1} + ';' + ", customBlock.inputs[inputIndex], customBlock.inputs[inputIndex]);
        }
        return variables;
    };

    presenter.extractInputsValuesFromBlock = function (customBlock, code) {
        for (var inputKey in customBlock.inputs) {
            if (customBlock.inputs.hasOwnProperty(inputKey)) {
                code += StringUtils.format("var {0} = Blockly.JavaScript.valueToCode(block, '{1}', Blockly.JavaScript.ORDER_ATOMIC);", customBlock.inputs[inputKey], customBlock.inputs[inputKey]);
            }
        }
        return code;
    };

    presenter.addVariablesToCode = function (customBlock, code, variables) {
        if (customBlock.connection.toUpperCase() === "LEFT") {
            return code + StringUtils.format("var code = [eval('(function Blockly_createCode_function_creating (){{0}}())'), Blockly.JavaScript.ORDER_ATOMIC];", customBlock.code);
        } else {
            return code + StringUtils.format("var code = {0}'{1}';", variables, customBlock.code);
        }
    };

    presenter.createBlockDefinition = function Blockly_createBlockDefinition_function (customBlock) {
        var blockDefinition = StringUtils.format("Blockly.Blocks['{0}'] = { ", customBlock.name)
            + "init: function Blockly_init_function_creating() {";

        if (customBlock.title != null) {
            blockDefinition += StringUtils.format("this.appendDummyInput().appendField('{0}');", customBlock.title);
        }

        for (var inputKey in customBlock.inputs) {
            if (customBlock.inputs.hasOwnProperty(inputKey)) {
                blockDefinition += StringUtils.format("this.appendValueInput('{0}')", customBlock.inputs[inputKey]);
                blockDefinition += StringUtils.format(".setCheck('{0}')", customBlock.inputsType[inputKey]);
                blockDefinition += StringUtils.format(".appendField('{0}');", customBlock.inputsText[inputKey]);
            }
        }

        if (customBlock.connection.toUpperCase() != "NONE" &amp;&amp; customBlock.connection.toUpperCase() != "TOP-BOTTOM") {
            blockDefinition += presenter.connections[customBlock.connection.toUpperCase()](customBlock.connectionType[0]);
        } else if (customBlock.connection.toUpperCase() == "TOP-BOTTOM") {
            blockDefinition += presenter.connections["TOP-BOTTOM"](customBlock.connectionType[0], customBlock.connectionType[1]);
        }

        blockDefinition += StringUtils.format("this.setColour({0});", customBlock.color);
        blockDefinition += StringUtils.format("this.setTooltip('');}};");

        return blockDefinition;
    };

    presenter.convertCustomBlocksToJS = function Blockly_convertCustomBlocksToJS_function (customBlocks) {
        var stringJS = {
            blockDefinitions: "",
            code: ""
        };

        for (var key in customBlocks) {
            if (customBlocks.hasOwnProperty(key)) {
                var convertedCode = presenter.convertCustomBlockToJS(customBlocks[key]);
                stringJS.blockDefinitions += convertedCode.blockDefinitions;
                stringJS.code += convertedCode.code;
            }
        }

        return stringJS;
    };

    presenter.validateCustomBlocks = function Blockly_validateCustomBlocks_function (customBlocksList) {
        var blocks = [];
        var names = [];

        for (var customBlockKey in customBlocksList) {
            if (customBlocksList.hasOwnProperty(customBlockKey)) {
                if (ModelValidationUtils.isStringEmpty(customBlocksList[customBlockKey]['customBlockName'])) {
                    continue;
                }
                var customBlock = customBlocksList[customBlockKey];
                var validatedBlock = presenter.validateBlock(customBlock);
                if (!validatedBlock.isValid) {
                    return validatedBlock;
                }
                blocks.push(validatedBlock);
            }
        }

        for (var i = 0; i &lt; blocks.length; i++) {
            if (names.indexOf(blocks[i].name) !== -1) {
                return {
                    isValid: false,
                    errorCode: 'DN01'
                }
            }
            names.push(blocks[i].name);
        }

        return {
            isValid: true,
            value: blocks
        };
    };

    presenter.connectHandlers = function Blockly_connectHandlers_function() {
        var $runButton = presenter.$view.find(".blockly-code-editor-run");
        $runButton.click(function Blockly_runClick_function () {
            if (presenter.configuration.sceneModule !== null) {
                var code = Blockly.JavaScript.workspaceToCode(presenter.configuration.workspace);
                presenter.configuration.sceneModule.executeCode(code);
            }
        });
    };

    presenter.setRunButton = function Blockly_setRunButton_function () {
        if (presenter.configuration.hideRun) {
            var $runButton = presenter.$view.find(".blockly-code-editor-run");
            $runButton.css({
                "display": "none"
            });
        }
    };

    presenter.executeCommand = function Blockly_executeCommand_function (name, params) {
        var commands = {
            'getWorkspaceCode' : presenter.getWorkspaceCode,
            'show' : presenter.show,
            'hide' : presenter.hide,
            'getConfiguration' : presenter.getConfiguration
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.hide = function Blockly_hide_function () {
        presenter.setVisibility(false);
    };

    presenter.show = function Blockly_show_function () {
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function Blockly_setVisibility_function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        var $blocklyToolboxDiv = $('.blocklyToolboxDiv');
        $blocklyToolboxDiv.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.getWorkspaceCode = function Blockly_getWorkspaceCode_function () {
        return Blockly.JavaScript.workspaceToCode(presenter.configuration.workspace);
    };

    presenter.getToolboxXML = function Blockly_getToolboxXML_function () {
        var toolbox = '&lt;xml&gt;';
        toolbox = StringUtils.format("{0}{1}", toolbox, generateXMLFromCategories(presenter.configuration.toolboxXML));
        toolbox = StringUtils.format("{0}{1}", toolbox, generateXMLWithoutCategories(presenter.configuration.toolboxXML));
        toolbox = StringUtils.format("{0}{1}", toolbox, "&lt;/xml&gt;");
        return toolbox;
    };

    presenter.getPreviewToolbox = function () {
        var toolbox = "&lt;xml&gt;";
        toolbox += "&lt;block type=\"lists_create_with\"&gt;";
        toolbox += "&lt;mutation items=\"3\"&gt;&lt;/mutation&gt;";
        toolbox += "&lt;/block&gt;";
        toolbox += "&lt;block type=\"controls_repeat_ext\"&gt;";
        toolbox += "&lt;value name=\"TIMES\"&gt;";
        toolbox += "&lt;shadow type=\"math_number\"&gt;";
        toolbox += "&lt;field name=\"NUM\"&gt;10&lt;/field&gt;";
        toolbox += "&lt;/shadow&gt;";
        toolbox += "&lt;/value&gt;";
        toolbox += "&lt;/block&gt;";
        toolbox += "&lt;block type=\"math_constant\"&gt;";
        toolbox += "&lt;field name=\"CONSTANT\"&gt;PI&lt;/field&gt;";
        toolbox += "&lt;/block&gt;";
        toolbox += "&lt;block type=\"controls_for\"&gt;";
        toolbox += "&lt;field name=\"VAR\"&gt;i&lt;/field&gt;";
        toolbox += "&lt;value name=\"FROM\"&gt;";
        toolbox += "&lt;shadow type=\"math_number\"&gt;";
        toolbox += "&lt;field name=\"NUM\"&gt;1&lt;/field&gt;";
        toolbox += "&lt;/shadow&gt;";
        toolbox += "&lt;/value&gt;";
        toolbox += "&lt;value name=\"TO\"&gt;";
        toolbox += "&lt;shadow type=\"math_number\"&gt;";
        toolbox += "&lt;field name=\"NUM\"&gt;10&lt;/field&gt;";
        toolbox += "&lt;/shadow&gt;";
        toolbox += "&lt;/value&gt;";
        toolbox += "&lt;value name=\"BY\"&gt;";
        toolbox += "&lt;shadow type=\"math_number\"&gt;";
        toolbox += "&lt;field name=\"NUM\"&gt;1&lt;/field&gt;";
        toolbox += "&lt;/shadow&gt;";
        toolbox += "&lt;/value&gt;";
        toolbox += "&lt;/block&gt;";
        toolbox += "&lt;/xml&gt;";

        return toolbox;
    };

    function generateXMLWithoutCategories (categories) {
        var toolboxText = "";
        if (categories.hasOwnProperty("Empty")) {
            for (var key in categories["Empty"]) {
                if (categories["Empty"].hasOwnProperty(key)) {
                    if (!categories["Empty"][key].isCategory) {
                        toolboxText = StringUtils.format("{0}&lt;block type='{1}'&gt;&lt;/block&gt;", toolboxText, categories["Empty"][key].name);
                    }
                }
            }
        }
        return toolboxText;
    }

    function generateXMLFromCategories(categories) {
        var xml = "";
        var variables_editor_xml = "";
        for (var categoryName in categories) {
            var category = "";
            var categoryIsNotEmpty = false;
            if (categories.hasOwnProperty(categoryName)) {
                category += "&lt;category name='" + categoryName + "' &gt;";
                for (var key in categories[categoryName]) {
                    if (categories[categoryName].hasOwnProperty(key)) {
                        var block = categories[categoryName][key];
                        if (block.name === 'variables_editor') {
                            variables_editor_xml = "&lt;category name='" + categoryName + "' custom='VARIABLE'&gt;&lt;/category&gt;";
                            continue;
                        }
                        if (!block.isCategory) {
                            continue;
                        }
                        categoryIsNotEmpty = true;
                        category += "&lt;block type='" + block.name + "'&gt;&lt;/block&gt;";
                    }
                }
            }
            category += "&lt;/category&gt;";
            if (categoryIsNotEmpty) {
                xml += category;
            }
        }
        xml += variables_editor_xml;
        return xml;
    }

    presenter.onEventReceived = function Blockly_onEventReceived_function (eventName, eventData) {
        if (eventName == "PageLoaded") {
            if (presenter.configuration.pageLoaded ) {
                return;
            }
            presenter.addBlocksTranslations();
            presenter.getSceneModuleOnPageLoaded();
            var sceneData = presenter.getSceneData();
            if (sceneData !== undefined) {
                if (presenter.configuration.addSceneToolbox) {
                    presenter.scenesInitialization[sceneData.type](sceneData);
                    presenter.updateToolbox();
                }
            }

            presenter.configuration.pageLoaded = true;
        } else if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };


    presenter.getSceneData = function Blockly_getSceneData () {
        if ((presenter.configuration.sceneModule !== null &amp;&amp; presenter.configuration.sceneModule !== undefined)) {
            if (presenter.configuration.sceneModule.getBlocklyData !== undefined &amp;&amp; presenter.configuration.sceneModule.getBlocklyData !== null) {
                return presenter.configuration.sceneModule.getBlocklyData();
            }
        }

        return undefined;
    };

    presenter.getSceneModuleOnPageLoaded = function Blockly_getSceneModuleOnPageLoaded_function () {
        if (presenter.configuration.sceneModule === null || presenter.configuration.sceneModule === undefined) {
            presenter.configuration.sceneModule = presenter.playerController.getModule(presenter.configuration.sceneID);
        }
    };

    presenter.updateToolbox = function Blockly_updateToolbox_function () {
        presenter.configuration.workspace.updateToolbox(presenter.getToolboxXML());
        var transformValueWidth = $(".blocklyToolboxDiv").width();

        presenter.$view.find('.blocklyFlyout').css({
            '-webkit-transform' : StringUtils.format("translate({0}px, 0)", transformValueWidth),
            '-moz-transform'    : StringUtils.format("translate({0}px, 0)", transformValueWidth),
            '-ms-transform'     : StringUtils.format("translate({0}px, 0)", transformValueWidth),
            '-o-transform'      : StringUtils.format("translate({0}px, 0)", transformValueWidth),
            'transform'         : StringUtils.format("translate({0}px, 0)", transformValueWidth)
        });
    };

    presenter.getState = function Blockly_Code_Edtior_get_state () {
        if (presenter.configuration.workspace == null) return '';
        var xml = Blockly.Xml.workspaceToDom(presenter.configuration.workspace);
        var value = {
            code: Blockly.Xml.domToText(xml),
            isVisible: presenter.configuration.isVisible
        };
        return JSON.stringify(value);
    };

    presenter.setState = function Blockly_Code_Editor_set_state (state) {
        if (presenter.configuration.workspace == null) {
            // state should only be set if the workspace is loaded
            presenter.savedRawState = state;
            return;
        }
        var value = JSON.parse(state);
        var xml = Blockly.Xml.textToDom(value.code);
        Blockly.Xml.domToWorkspace(xml, presenter.configuration.workspace);
        presenter.setVisibility(value.isVisible);
    };

    presenter.reset = function Blockly_reset_function () {
        presenter.setWorkMode();
        presenter.configuration.workspace.clear();
    };

    presenter.setShowErrorsMode = function Blockly_setShowErrorsMode_function () {
        presenter.setWorkMode();
        presenter.coverToolbox();
        presenter.coverAddon();
    };

    presenter.setWorkMode = function Blockly_setWorkMode_function () {
        presenter.removeToolboxCover();
        presenter.removeAddonCover();
    };

    presenter.showAnswers = function Blockly_showAnswers_function () {
        presenter.setShowErrorsMode();
    };

    presenter.hideAnswers = function Blockly_hideAnswers_function () {
        presenter.setWorkMode();
    };

    presenter.coverAddon = function Blockly_coverAddon_function () {
        var cover = $("&lt;div&gt;");
        cover.addClass("blockly-cover");
        presenter.$view.append(cover);
    };

    presenter.removeAddonCover = function Blockly_removeAddonCover_function () {
        var element = presenter.$view.find('.blockly-cover');
        element.remove();
    };

    presenter.coverToolbox = function Blockly_coverToolbox_function () {
        var toolbox = presenter.view.getElementsByClassName('blocklyToolboxDiv');
        if (toolbox.length != 0) {
            var cover = document.createElement('div');
            cover.className = 'toolboxCover blockly-cover';
            toolbox[0].appendChild(cover);
        }
    };

    presenter.removeToolboxCover = function Blockly_removeToolboxCover_function () {
        var toolboxCover = presenter.view.getElementsByClassName('toolboxCover blockly-cover');
        if (toolboxCover !== null &amp;&amp; toolboxCover !== undefined) {
            if (toolboxCover.length &gt; 0) {
                toolboxCover[0].parentNode.removeChild(toolboxCover[0]);
            }
        }
    };

    presenter.gridSceneInitialization = function (data) {
        for (var key in data.labels) {
            if (data.labels.hasOwnProperty(key) &amp;&amp; presenter.configuration.blocksTranslation.hasOwnProperty(key)) {
                data.labels[key] = presenter.configuration.blocksTranslation[key];
            }
        }
        BlocklyCustomBlocks.SceneGrid.addBlocks(data.labels);

        if (presenter.configuration.addSceneToolbox) {
            for (var key in data.availableBlocks) {
                if (data.availableBlocks.hasOwnProperty(key)) {
                    presenter.configuration.toolboxXML[presenter.configuration.sceneToolboxName].push({
                        name: key,
                        isCategory: presenter.configuration.sceneToolboxIsCategory
                    });
                }
            }
        }
    };

    presenter.scenesInitialization = {
        "GridScene" : presenter.gridSceneInitialization
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Board_Game" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Background" name="Background" nameLabel="Board_game_property_background" type="image"/>
        <property displayName="has Fields" name="hasFields" nameLabel="Board_game_property_has_fields" type="boolean"/>
        <property isDefault="true" name="Fields" nameLabel="Board_game_property_fields" type="list">
            <property name="Top" nameLabel="Board_game_property_top" type="string"/>
            <property name="Left" nameLabel="Board_game_property_left" type="string"/>
            <property name="Width" nameLabel="Board_game_property_width" type="string"/>
            <property name="Height" nameLabel="Board_game_property_height" type="string"/>
            <property name="cssClass" nameLabel="Board_game_property_css_class" type="string"/>
        </property>
        <property displayName="Elements" name="Images" nameLabel="Board_game_property_images" type="list">
            <property displayName="Element Image" name="PawnImage" nameLabel="Board_game_property_pawn_image" type="image"/>
            <property name="Top" nameLabel="Board_game_property_top" type="string"/>
            <property name="Left" nameLabel="Board_game_property_left" type="string"/>
            <property name="Width" nameLabel="Board_game_property_width" type="string"/>
            <property name="Height" nameLabel="Board_game_property_height" type="string"/>
        </property>
        <property name="isDisabled" nameLabel="Board_game_property_is_disable" type="boolean"/>
        <property name="gameMode" nameLabel="Board_game_property_game_mode" type="{Free,Game}"/>
	</model>
<css>.board-game-container{
    background-color:white;
    -moz-background-size:100% 100%;
    -webkit-background-size:100% 100%;
    background-size:100% 100%;
    background-repeat: no-repeat;
}

.board-game-element{
    position:absolute;
    background-color:blue;
}

.counters-container {
    position:absolute;
}

.board-game-field{
    position:absolute;
    background-color:blue;
    opacity: 0.3;
}

.board-game-field.game {
    background-color: transparent;
    opacity: 1;
}

.board-game-element.game {
    position: static;
    float: left;
    box-sizing: border-box;
}
</css><view>&lt;p class="board-game-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id="board-game-container" class="board-game-container"&gt;&lt;/div&gt;
</view><preview>&lt;p class="board-game-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id="board-game-container" class="board-game-container"&gt;&lt;/div&gt;
</preview><presenter>/**
 * In game mode, counter elements are in first container. If element is moving, he got absolute position and is moved to
 * view. After animation, animated element must be moved to container as child and must receive static positioning.
 * @returns {presenter}
 * @constructor
 */
function AddonBoard_Game_create(){

    var presenter = function() {
    };

    presenter.ERROR_CODES = {
        'F01': 'Field is outside of addon!',
        'I01': 'Element is outside of addon!',
        "CSS01": "Provided css class name is not valid class name",
        "EV01": "Provided game type is not valid",
    };

    presenter.ANIMATION_MOVE_TIME = 300;    //Time for one field animation in ms
    presenter.currentElement = 0;
    presenter.mouseX = 0;
    presenter.mouseY = 0;
    presenter.isElementInMove = false;
    presenter.showAnswersMode = false;
    presenter.isDisabled = false;
    presenter.hasFields = false;
    presenter.currentLeftValue = [];
    presenter.currentTopValue = [];
    presenter.originalLeftValue = [];
    presenter.originalTopValue = [];
    presenter.lastSelectedCounter = null;
    presenter.boardCounters = [];
    presenter.fields = [];
    presenter.fieldsPositions = [];
    presenter.gameTypes = {
        GAME: "Game",
        FREE: "Free"
    };
    presenter.counterPositions = [];
    /**
     * @type {{counterNumber: Number, position: Number}|null}
     */
    presenter.lastMove = null;
    


    presenter.executeCommand = function(name, params) {
        switch (name.toLowerCase()) {
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'move'.toLowerCase():
                presenter.move(params[0]);
                break;
            case 'undo'.toLowerCase():
                presenter.undo();
                break
        }
    };

    presenter.drawBoard = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.Height = model.Height;
        presenter.Width = model.Width;

        presenter.$view.find('.board-game-container').css({
            'width' : presenter.Width,
            'height' : presenter.Height,
            'background-image' : 'url('+ model.Background + ')'
        });

        var fig = '';
        var i;

        if(model.hasFields || presenter.gameMode !== presenter.gameTypes.FREE){
            for(i = 0; i &lt; presenter.fieldsLength; i++){
                fig += '&lt;div id="Field' + (i + 1) + '" class="board-game-field" style=""&gt;&lt;/div&gt;';
                fig += '&lt;div class="counters-container" style=""&gt;&lt;/div&gt;';
            }
        }
        for(i = 0; i &lt; model.Images.length; i++){
            fig += '&lt;div id="Element' + (i + 1) +'" class="board-game-element" style="background-image: url(' + model.Images[i].PawnImage + '); -moz-background-size:100% 100%; -webkit-background-size:100% 100%; background-size:100% 100%;; height: ' + model.Images[i].Height + 'px; width: ' + model.Images[i].Width + 'px;  "&gt;&lt;/div&gt;';
            presenter.currentLeftValue[i] = model.Images[i].Left;
            presenter.currentTopValue[i] = model.Images[i].Top;
            presenter.originalLeftValue[i] = model.Images[i].Left;
            presenter.originalTopValue[i] = model.Images[i].Top;
        }

        return fig;
    };

    presenter.upgradeFields = function (model) {
        var fields = model["Fields"];

        fields.forEach(function (element) {
            if (!element["cssClass"]) {
                element["cssClass"] = "";
            }
        });

        return model;
    };

    presenter.upgradeModel = function (model) {
        if (!model["isDisabled"]) {
            model["isDisabled"] = "False";
        }

        if (!model["gameMode"]) {
            model["gameMode"] = "Free";
        }

        model = presenter.upgradeFields(model);

        return model;
    };

    presenter.validateFieldsSizes = function (model) {
        var i,
            field;

        for(i = 0; i &lt; model.Fields.length; i++) {
            field = model.Fields[i];
            if (field.Left + field.Width &gt; model.Width || field.Top + field.Height &gt; model.Height) {
                return {
                    isValid: false,
                    errorCode: "F01"
                };
            }
        }

        return  {
            isValid: true
        };
    };

    presenter.validateImagesSizes = function (model) {
        var i,
            image;

        for(i = 0; i &lt; model.Images.length; i++) {
            image = model.Images[i];
            if (image.Left + image.Width &gt; model.Width || image.Top + image.Height &gt; model.Height) {
                return {
                    isValid: false,
                    errorCode: "I01"
                };
            }
        }

        return {
            isValid: true
        };
    };

    function _animate(element, xPosition, yPosition, callback) {
        $(element).animate({
            left: xPosition,
            top: yPosition
        }, presenter.ANIMATION_MOVE_TIME, callback);
    }

    presenter.endAnimation = function (endPositionIndex, counter, counterIndex) {
        var destXPosition,
            destYPosition;

        destXPosition = presenter.fieldsPositions[endPositionIndex].Left;
        destYPosition = presenter.fieldsPositions[endPositionIndex].Top;

        $(counter).css({
            left: destXPosition + "px",
            top: destYPosition + "px"
        });

        presenter.countersContainers[endPositionIndex].appendChild(counter);
        counter.style.position = 'static';

        presenter.sendEventCounterMoved(counterIndex, endPositionIndex);
    };

    presenter.animate = function (counterNumber, startPositionIndex, endPositionIndex, runAnimation) {
        var destXPosition,
            destYPosition,
            counter,
            nextElement;

        if (!runAnimation) {
            startPositionIndex = endPositionIndex;
        }

        counter = presenter.boardCounters[counterNumber];
        if (startPositionIndex === endPositionIndex) {
            presenter.endAnimation(endPositionIndex, counter, counterNumber);
            return;
        }

        if (startPositionIndex &lt; endPositionIndex) {
            nextElement = startPositionIndex + 1;
        } else {
            nextElement = startPositionIndex - 1;
        }

        destXPosition = presenter.fieldsPositions[nextElement].Left;
        destYPosition = presenter.fieldsPositions[nextElement].Top;

        counter.style.position = 'absolute';

        _animate(counter, destXPosition, destYPosition, function () {
            presenter.animate(counterNumber, nextElement, endPositionIndex, true);
        });
    };

    presenter.validateModel = function (model) {
        model = presenter.upgradeModel(model);

        var modelValidator = new ModelValidator();

        var validatedModel = modelValidator.validate(model, [
            ModelValidators.Boolean("hasFields"),
            ModelValidators.Enum("gameMode", {values: ["Free", "Game"], useLowerCase: false}),
            ModelValidators.List("Fields", [
                ModelValidators.DumbInteger("Top"),
                ModelValidators.DumbInteger("Left"),
                ModelValidators.DumbInteger("Width"),
                ModelValidators.DumbInteger("Height"),
                ModelValidators.CSSClass("cssClass", {default: ""})
            ], function () {
                return this.validatedModel['hasFields'] || this.validatedModel["gameMode"] !== presenter.gameTypes.FREE;
            }),
            ModelValidators.List("Images", [
                ModelValidators.DumbInteger("Top"),
                ModelValidators.DumbInteger("Left"),
                ModelValidators.DumbInteger("Width"),
                ModelValidators.DumbInteger("Height"),
                ModelValidators.DumbString("PawnImage")
            ]),
            ModelValidators.Boolean("isDisabled"),
            ModelValidators.DumbInteger("Width"),
            ModelValidators.DumbInteger("Height"),
            ModelValidators.DumbString("ID"),
            ModelValidators.Boolean("Is Visible"),
            ModelValidators.DumbString("Background"),
        ]);

        if (!validatedModel.isValid) {
            return validatedModel;
        }

        var validatedFields,
            validatedImages;

        if (validatedModel.value["hasFields"] || validatedModel.value["gameMode"] !== presenter.gameTypes.FREE) {
            validatedFields = presenter.validateFieldsSizes(validatedModel.value);
            if (!validatedFields.isValid) {
                return validatedFields;
            }
        }

        validatedImages = presenter.validateImagesSizes(validatedModel.value);
        if (!validatedImages.isValid) {
            return validatedImages;
        }

        return validatedModel;

    };

    presenter.selectCounter = function (element, index) {
        presenter.deselectLastCounter();

        presenter.lastSelectedCounter = index;

        element.classList.add('board-game-selected')
    };

    presenter.deselectLastCounter = function () {
        if (presenter.lastSelectedCounter === null) {
            return;
        }

        presenter.boardCounters[presenter.lastSelectedCounter].classList.remove('board-game-selected');
    };

    presenter.connectHandlers = function (view) {
        if (presenter.gameMode === presenter.gameTypes.GAME) {
            presenter.boardCounters.each(function (index, element) {
                $(element).on('click', function () {
                    if (presenter.isDisabled || presenter.showErrorsMode || presenter.showAnswersMode) {
                        return;
                    }

                    presenter.selectCounter(this, index);
                });
            });
        }

        if (presenter.gameMode === presenter.gameTypes.FREE) {

            presenter.$view.find('.board-game-field').droppable({
                drop: function (e, ui) {
                    presenter.checkRevert(this);
                }
            });

            jQuery(function ($) {
                presenter.boardCounters
                    .mousedown(function (e) {
                        presenter.moveCurrentElement(this);
                    })
                    .mouseup(function (e) {}).click(function (e) {});
            });

            presenter.boardCounters.draggable({
                containment: "parent"
            });
        }
    };

    presenter.destroy = function (event) {
        if (event.target != presenter.$view.get(0)) {
            return;
        }

        clearInterval(presenter.interval);

        presenter.removeHandlers();
    };

    presenter.removeHandlers = function () {
        presenter.boardCounters.off();
        presenter.$view.find('.board-game-field').off();
    };

    presenter.getElementToNavigation = function () {
        var elementsToNavigation = [];
        presenter.boardCounters.each(function (index, element) {
            elementsToNavigation.push($(element));
        });

        return elementsToNavigation;
    };

    presenter.setFieldsSizes = function (model) {
        var i;

        for(i = 0; i &lt; presenter.fieldsLength; i++) {
            var cssValue = {
                'width' : model.Fields[i].Width + "px",
                'height' : model.Fields[i].Height + "px",
                'top' : model.Fields[i].Top + "px",
                'left' : model.Fields[i].Left + "px"
            };

            $(presenter.fields[i]).css(cssValue);
            $(presenter.countersContainers[i]).css(cssValue);
        }
    };

    presenter.moveCountersToFirstField = function () {
        var i,
            destXPosition = presenter.fieldsPositions[0].Left,
            destYPosition = presenter.fieldsPositions[0].Top;
        presenter.counterPositions = [];

        for (i = 0; i &lt; presenter.boardCounters.length; i++) {
            presenter.counterPositions.push(0);

            presenter.boardCounters[i].classList.add('game');
            presenter.boardCounters[i].style.position = 'static';
            $(presenter.boardCounters[i]).css({
                left: destXPosition + 'px',
                top: destYPosition + 'px'
            });
            presenter.countersContainers[0].appendChild(presenter.boardCounters[i]);
        }
    };

    presenter.initGameMode = function (model) {
        presenter.setFieldsSizes(model);
        presenter.moveCountersToFirstField();
        presenter.selectCounter(presenter.boardCounters[0], 0);

        presenter.boardGameKeyboardController = new BoardGameGameModeKeyboardController(presenter.getElementToNavigation());
    };

    presenter.initFreeMode = function (model) {
        if (presenter.hasFields) {
            presenter.setFieldsSizes(model);
        }

        presenter.boardGameKeyboardController = new BoardGameFreeModeKeyboardController(presenter.getElementToNavigation());
        presenter.boardGameKeyboardController.mapping[KeyboardControllerKeys.ARROW_LEFT] = presenter.boardGameKeyboardController.left;
        presenter.boardGameKeyboardController.mapping[KeyboardControllerKeys.ARROW_RIGHT] = presenter.boardGameKeyboardController.right;
        presenter.boardGameKeyboardController.mapping[KeyboardControllerKeys.ARROW_UP] = presenter.boardGameKeyboardController.up;
        presenter.boardGameKeyboardController.mapping[KeyboardControllerKeys.ARROW_DOWN] = presenter.boardGameKeyboardController.down;
        presenter.boardGameKeyboardController.mapping[KeyboardControllerKeys.TAB] = presenter.boardGameKeyboardController.tab;
    };

    presenter.init = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.modelID = model.ID;
        presenter.isDisable = model.isDisabled;
        presenter.wasDisable = model.isDisabled;
        presenter.hasFields = model.hasFields;
        presenter.wasVisible = model["Is Visible"];
        presenter.isVisible = model["Is Visible"];
        presenter.fieldsLength = model.Fields ? model.Fields.length : 0;
        presenter.imagesLength = model.Images.length;
        presenter.gameMode = model.gameMode;
        presenter.fieldsPositions = model.Fields;
        presenter.interval = 0;

        presenter.lastSelectedCounter = null;

        var myDiv = $(view).find('.board-game-container')[0];
        var board = presenter.drawBoard(view, model);
        $(myDiv).append(board);

        presenter.setVisibility(presenter.isVisible);

        presenter.boardCounters = presenter.$view.find('.board-game-element');
        presenter.fields = presenter.$view.find('.board-game-field');
        presenter.countersContainers = presenter.$view.find('.counters-container');


        if (presenter.gameMode === presenter.gameTypes.GAME) {
            presenter.initGameMode(model);
        } else {
            presenter.setElementsPosition(presenter.originalLeftValue, presenter.originalTopValue);
            presenter.initFreeMode(model);
        }

        presenter.setFieldsClasses(model);
        presenter.connectHandlers(view);

        presenter.view = view;
    };

    presenter.setFieldsClasses = function (model) {
        if (!model.Fields) {
            return;
        }

        model.Fields.forEach(function (element, idx) {
            if (element.cssClass !== "") {
                presenter.fields[idx].classList.add(element.cssClass);
            }
        });
    };

    presenter.showErrorMessage = function (view, error) {
        var $counter = $(view).find('.board-game-counter');
        $counter.text(presenter.ERROR_CODES[error.errorCode]);
    };

    presenter.run = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        var validatedModel = presenter.validateModel(model);

        if (validatedModel.isValid) {
            presenter.init(view, validatedModel.value);
            MutationObserverService.createDestroyObserver(presenter.modelID, presenter.destroy, presenter.$view.get(0));
            MutationObserverService.setObserver();

            presenter.eventBus.addEventListener('ShowAnswers', this);
            presenter.eventBus.addEventListener('HideAnswers', this);

            if(presenter.isDisable){
                presenter.disable();
            }
        } else {
            presenter.showErrorMessage(view, validatedModel);
        }
    };

    presenter.moveCurrentElement = function(element) {
        presenter.currentElement = element.id;
        if(presenter.hasFields) {
            presenter.$view.find('#' + presenter.currentElement + '').draggable({ revert: true });
        }
    };

    presenter.checkRevert = function(element){
        var field = element.id;
        presenter.$view.find('#' + presenter.currentElement + '').draggable({ revert: false });
        presenter.triggerFrameChangeEvent(field, presenter.currentElement);
    };

    presenter.createPreview = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        var validatedModel = presenter.validateModel(model);
        if (!validatedModel.isValid) {
            presenter.showErrorMessage(view, validatedModel);
            return;
        }

        presenter.init(view, model);
        $(view).find('.board-game-element').draggable({ containment: "parent" });

        var coordinations = {x: 0, y: 0};

        var coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
            xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
            yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;');
        coordinatesContainer.addClass('coordinates');
        coordinatesContainer.append(xContainer).append(yContainer);
        $(view).find('.board-game-container').append(coordinatesContainer);

        function setCalculatedPosition(e) {
            coordinations.x = e.originalEvent.pageX || e.originalEvent.touches[0].pageX;
            coordinations.y = e.originalEvent.pageY || e.originalEvent.touches[0].pageY;
            presenter.mouseSX = parseInt(coordinations.x, 10) - parseInt($(view).find('.board-game-container').offset().left,10);
            presenter.mouseSY = parseInt(coordinations.y, 10) - parseInt($(view).find('.board-game-container').offset().top,10);
            xContainer.find('.value').html(presenter.mouseSX);
            yContainer.find('.value').html(presenter.mouseSY);
        }

        var doesElementExist = function() {
            var $moduleSelector = $('.moduleSelector[data-id="' + presenter.modelID + '"]');

            if ($moduleSelector.length &gt; 0) {
                $moduleSelector.on('mousemove', function(e) {
                    setCalculatedPosition(e);
                });

                clearInterval(presenter.interval);
            }
        };

        presenter.interval = setInterval(function() { doesElementExist(); }, 500);

        $(view).find('.board-game-container').on('mousemove', function(e) {
            setCalculatedPosition(e);
        });

    };

    presenter.disable = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        presenter.isDisable = true;
        var $myDiv = presenter.$view.find('.board-game-container')[0];
        $($myDiv).addClass('disable');
        presenter.$view.find('.board-game-element').draggable("disable");
    };

    presenter.enable = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        presenter.isDisable = false;
        var $myDiv = presenter.$view.find('.board-game-container')[0];
        $($myDiv).removeClass('disable');
        presenter.$view.find('.board-game-element').draggable("enable");
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        presenter.setVisibility(false);
        presenter.isVisible = false;
    };


    presenter.setElementsPosition = function(arrayLeft, arrayTop){
        var i;

        for(i = 0; i &lt; presenter.imagesLength; i++){
            $(presenter.boardCounters[i]).css({
                'left' : arrayLeft[i] + "px",
                'top' : arrayTop[i] + "px"
            });
        }
    };

    presenter.getElementsPosition = function(){
        var i;

        for(i = 0; i &lt; presenter.imagesLength; i++) {
            presenter.currentLeftValue[i] = parseInt($(presenter.$view.find('.board-game-element')[i]).css("left"),10);
            presenter.currentTopValue[i] = parseInt($(presenter.$view.find('.board-game-element')[i]).css("top"),10);
        }
    };

    presenter.getState = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        var isVisible = presenter.isVisible;
        var wasVisible = presenter.wasVisible;
        var wasDisable = presenter.wasDisable;
        var isDisable = presenter.isDisable;

        var additionalState = {};

        if (presenter.gameTypes.FREE === presenter.gameMode) {
            additionalState = presenter.getStateForFreeMode();
        } else {
            additionalState = presenter.getStateForGameMode();
        }

        return JSON.stringify($.extend({
            isVisible : isVisible,
            wasVisible : wasVisible,
            wasDisable : wasDisable,
            isDisable : isDisable
        }, additionalState));

    };

    presenter.getStateForFreeMode = function () {
        presenter.getElementsPosition();

        var originalLeftValue = presenter.originalLeftValue;
        var originalTopValue = presenter.originalTopValue;
        var currentLeftValue = presenter.currentLeftValue;
        var currentTopValue = presenter.currentTopValue;

        return {
            originalLeftValue : originalLeftValue,
            originalTopValue : originalTopValue,
            currentLeftValue : currentLeftValue,
            currentTopValue : currentTopValue,
        };
    };

    presenter.getStateForGameMode = function () {
        return {
            counterPositions: presenter.counterPositions,
            lastSelectedCounter: presenter.lastSelectedCounter,
            lastMove: presenter.lastMove
        };
    };

    presenter.setState = function(state) {
        var parsedState = JSON.parse(state), $myDiv = presenter.$view.find('.board-game-container')[0];

        presenter.isVisible = parsedState.isVisible;
        presenter.wasVisible = parsedState.wasVisible;
        presenter.wasDisable = parsedState.wasDisable;
        presenter.isDisable = parsedState.isDisable;
        presenter.setVisibility(presenter.isVisible);

        if(presenter.isDisable){
            $($myDiv).addClass('disable');
        } else{
            $($myDiv).removeClass('disable');
        }

        if (presenter.gameMode === presenter.gameTypes.FREE) {
            presenter.setStateForFreeMode(parsedState);
        } else {
            presenter.setStateForGameMode(parsedState);
        }
    };

    presenter.setStateForFreeMode = function (parsedState) {
        presenter.originalLeftValue = parsedState.originalLeftValue;
        presenter.originalTopValue = parsedState.originalTopValue;
        presenter.currentLeftValue = parsedState.currentLeftValue;
        presenter.currentTopValue = parsedState.currentTopValue;

        if(presenter.isDisable){
            presenter.$view.find('.board-game-element').draggable("disable");
        } else{
            presenter.$view.find('.board-game-element').draggable("enable");
        }

        presenter.setElementsPosition(presenter.currentLeftValue, presenter.currentTopValue);

    };

    presenter.setStateForGameMode = function (parsedState) {
        presenter.boardCounters.each(function (index, element) {
            presenter.selectCounter(element, index);
            presenter.moveCounter(parsedState.counterPositions[index], false);
        });

        presenter.selectCounter(presenter.boardCounters[parsedState.lastSelectedCounter], parsedState.lastSelectedCounter);
        presenter.lastMove = parsedState.lastMove;
    };

    presenter.reset = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        presenter.setWorkMode();

        presenter.isVisible = presenter.wasVisible;
        presenter.setVisibility(presenter.wasVisible);
        presenter.isDisable = presenter.wasDisable;

        var $myDiv = presenter.$view.find('.board-game-container')[0];

        if(presenter.isDisable){
            $($myDiv).addClass('disable');
        } else{
            $($myDiv).removeClass('disable');
        }

        if (presenter.gameMode === presenter.gameTypes.FREE) {
            presenter.resetFreeGame();
        } else {
            presenter.resetGameMode();
        }
    };

    presenter.resetFreeGame = function () {
        presenter.setElementsPosition(presenter.originalLeftValue, presenter.originalTopValue);

        if (presenter.isDisable) {
            presenter.$view.find('.board-game-element').draggable("disable");
        } else {
            presenter.$view.find('.board-game-element').draggable("enable");
        }
    };

    presenter.resetGameMode = function () {
        $(presenter.boardCounters).stop();
        presenter.moveCountersToFirstField();

        presenter.selectCounter(presenter.boardCounters[0], 0);
        presenter.lastMove = null;
    };

    presenter.setShowErrorsMode = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        presenter.showErrorsMode = true;
        var $myDiv = presenter.$view.find('.board-game-container')[0];
        $($myDiv).addClass('check');
        presenter.$view.find('.board-game-element').draggable("disable");
    };

    presenter.setWorkMode = function() {
        presenter.showErrorsMode = false;
        var $myDiv = presenter.$view.find('.board-game-container')[0];
        $($myDiv).removeClass('check');

        if(!presenter.isDisable){
            presenter.$view.find('.board-game-element').draggable("enable");
        }
    };

    presenter.showAnswers = function () {
        if(presenter.showErrorsMode === true){
            presenter.setWorkMode();
        }

        presenter.showAnswersMode = true;
        var $myDiv = presenter.$view.find('.board-game-container')[0];
        $($myDiv).addClass('show-answer');
        presenter.$view.find('.board-game-element').draggable("disable");
    };

    presenter.hideAnswers = function () {
        if(presenter.showAnswersMode === true){
            var $myDiv = presenter.$view.find('.board-game-container')[0];
            $($myDiv).removeClass('show-answer');

            if(!presenter.isDisable){
                presenter.$view.find('.board-game-element').draggable("enable");
            }

            presenter.showAnswersMode = false;
        }
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(eventItem, eventValue) {
        return {
            source : presenter.modelID,
            item : "" + eventItem,
            value : '' + eventValue,
            score : ''
        };
    };

    presenter.triggerFrameChangeEvent = function(eventItem, eventValue) {
        var eventData = presenter.createEventData(eventItem, eventValue);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.sendEventCounterMoved = function (counterIndex, position) {
        var eventData = presenter.createEventData(counterIndex + 1, position + 1);

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.moveCounter = function (distance, withAnimation) {
        if (withAnimation === undefined) {
            withAnimation = true;
        }

        var counterIndex = presenter.lastSelectedCounter,
            counterElementToMove = presenter.boardCounters[counterIndex],
            counterPosition = presenter.counterPositions[counterIndex],
            newCounterPosition = Math.min(presenter.countersContainers.length - 1, counterPosition + distance);

        newCounterPosition = Math.max(0, newCounterPosition);

        $(counterElementToMove).stop();

        presenter.counterPositions[counterIndex] = newCounterPosition;
        presenter.view.appendChild(counterElementToMove);
        presenter.lastMove = {
            counterNumber: counterIndex,
            position: counterPosition
        };

        var nextCounterPosition = (counterIndex + 1) % presenter.boardCounters.length;
        presenter.selectCounter(presenter.boardCounters[nextCounterPosition], nextCounterPosition);

        presenter.animate(counterIndex, counterPosition, newCounterPosition, withAnimation);
    };

    presenter.canExecuteAction = function () {
        if (presenter.isDisable || presenter.showErrorsMode || presenter.showAnswersMode) {
            return false;
        }

        if (presenter.gameMode !== presenter.gameTypes.GAME) {
            return false;
        }

        return true;
    };

    presenter.move = function (distance) {
        distance = parseInt(distance, 10);
        if (isNaN(distance)) {
            return;
        }

        if (!presenter.canExecuteAction()) {
            return;
        }

        presenter.moveCounter(distance);
    };

    presenter.undo = function () {
        if (!presenter.canExecuteAction()) {
            return;
        }

        if (presenter.lastMove === null) {
            return;
        }


        var counterElementToMove = presenter.boardCounters[presenter.lastMove.counterNumber];
        $(counterElementToMove).stop();

        presenter.selectCounter(counterElementToMove, presenter.lastMove.counterNumber);
        presenter.countersContainers[presenter.lastMove.position].appendChild(counterElementToMove);
        presenter.counterPositions[presenter.lastMove.counterNumber] = presenter.lastMove.position;
        presenter.setUndoCorrectCSSPosition();

        presenter.lastMove = null;
    };

    presenter.setUndoCorrectCSSPosition = function () {
        var destXPosition = presenter.fieldsPositions[presenter.lastMove.position].Left;
        var destYPosition = presenter.fieldsPositions[presenter.lastMove.position].Top;
        var counterElementToMove = presenter.boardCounters[presenter.lastMove.counterNumber];

        $(counterElementToMove).css({
            left: destXPosition + 'px',
            top: destYPosition + 'px',
            position: 'static'
        });
    };

    presenter.diceExecute = function (distance) {
        presenter.move(distance);
    };

    // Game mode controller
    function BoardGameGameModeKeyboardController(elements) {
        KeyboardController.call(this, elements, elements.length);
    }
    BoardGameGameModeKeyboardController.prototype = Object.create(KeyboardController.prototype);
    BoardGameGameModeKeyboardController.prototype.constructor = BoardGameGameModeKeyboardController;

    //Free mode controller

    /**
     * Get clientX and clientY positions based on element position
     * @param {jQuery} $element
     * @returns {{left: number, top: number}}
     */
    function getElementPosition ($element) {
        var top = $element.offset().top - document.documentElement.scrollTop,
            left = $element.offset().left - document.documentElement.scrollLeft;

        return {
            left: left,
            top: top
        };
    }

    /**
     * Build mouse event for simulating drag n drop
     * @param {"mousedown"|"mouseup"|"mousemove"} type
     * @param position {{top: Number, left: Number}}
     * @returns {MouseEvent}
     */
    function buildMouseEvent(type, position) {
        var event = document.createEvent("MouseEvent");
        event.initMouseEvent(type, true, true, window, 1, 0, 0, position.left, position.top, false, false, false, false, 0, null);

        return event;
    }

    function BoardGameFreeModeKeyboardController(elements) {
        KeyboardController.call(this, elements, elements.length);
    }

    BoardGameFreeModeKeyboardController.prototype = Object.create(KeyboardController.prototype);
    BoardGameFreeModeKeyboardController.prototype.constructor = BoardGameFreeModeKeyboardController;

    BoardGameFreeModeKeyboardController.prototype.left = function () {
        var position = getElementPosition(this.keyboardNavigationCurrentElement);
        position.left -= 2;

        var event = buildMouseEvent("mousemove", position);
        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.right = function () {
        var position = getElementPosition(this.keyboardNavigationCurrentElement);
        position.left += 2;

        var event = buildMouseEvent("mousemove", position);
        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.up = function () {
        var position = getElementPosition(this.keyboardNavigationCurrentElement);
        position.top -= 2;

        var event = buildMouseEvent("mousemove", position);
        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.down = function () {
        var position = getElementPosition(this.keyboardNavigationCurrentElement);
        position.top += 2;

        var event = buildMouseEvent("mousemove", position);
        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.tab = function (ev) {
        var position = getElementPosition(this.keyboardNavigationCurrentElement),
            event = buildMouseEvent("mouseup", position);

        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);

        KeyboardController.prototype.nextElement.call(this, ev);

        position = getElementPosition(this.keyboardNavigationCurrentElement);
        event = buildMouseEvent("mousedown", position);

        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.enter = function (ev) {
        KeyboardController.prototype.enter.call(this, ev);

        var position = getElementPosition(this.keyboardNavigationCurrentElement);
        var event = buildMouseEvent("mousedown", position);

        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);
    };

    BoardGameFreeModeKeyboardController.prototype.exitWCAGMode = function (ev) {
        var position = getElementPosition(this.keyboardNavigationCurrentElement),
            event = buildMouseEvent("mouseup", position);

        this.keyboardNavigationCurrentElement[0].dispatchEvent(event);

        KeyboardController.prototype.exitWCAGMode.call(this, ev);
    };

    presenter.keyboardController = function (keyCode, isShiftDown, originalEvent) {
        presenter.boardGameKeyboardController.handle(keyCode, isShiftDown, originalEvent);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Catch" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Items" nameLabel="Catch_property_items" type="list">
            <property name="Image" nameLabel="Catch_property_image" type="image"/>
            <property isLocalized="true" name="Description" nameLabel="Catch_property_description" type="string"/>
            <property name="Is Correct" nameLabel="Catch_property_is_correct" type="boolean"/>
            <property isLocalized="true" name="Level" nameLabel="Catch_property_level" type="string"/>
        </property>
        <property isLocalized="true" name="Item_Width" nameLabel="Catch_property_item_width" type="string"/>
        <property isLocalized="true" name="Item_Height" nameLabel="Catch_property_item_height" type="string"/>
        <property isLocalized="true" name="Points to finish" nameLabel="Catch_property_points_to_finish" type="string"/>
        <property name="Count errors" nameLabel="Catch_property_count_errors" type="boolean"/>
        <property name="Plate image" nameLabel="Catch_property_plate_image" type="image"/>
    </model>
<css>.addon_Catch {
    max-width: 100%;
    width: 100%;
    text-align: center;
    margin: 0 auto;
    /*border: 1px solid black;*/
    position: relative;
    overflow: hidden;
    box-sizing: border-box;
}

.addon_Catch .plate,
.addon_Catch .fallingObject,
.addon_Catch .description {
    position: absolute;
    box-sizing: border-box;
    -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
    -khtml-user-select: none; /* Konqueror HTML */
    -moz-user-select: none; /* Firefox */
    -ms-user-select: none; /* Internet Explorer/Edge */
    user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
}

.addon_Catch .plate {
    width: 50px;
    height: 25px;
    bottom: 0;
}

.addon_Catch .fallingObject {
    width: 30px;
    height: 30px;
}

.addon_Catch .welcome {
    width: 100%;
    height: 100%;
    cursor: pointer;
    background-size: auto;
}
</css><view/><preview/><presenter>function AddonCatch_create() {

    var presenter = function () {};
    presenter.configuration = {};

    var points = 0;
    var errors = 0;
    var $plateElement = null;
    var isGameOver = false;
    var isPaused = false;
    var pausedTimeInMs = 0;
    var objects = [];

    function getErrorObject (ec) { return { isValid: false, errorCode: ec }; }
    function getCorrectObject (v) { return { isValid: true, value: v }; }

    function getRandomInt (min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    function getItemObject (index, image, description, isCorrect, level) {
        return {
            index: index,             // item index
            image: image,             // image url
            description: description, // description (max 20 characters)
            isCorrect: isCorrect,     // boolean
            levels: level             // array of levels in range 1 - 3
        };
    }

    function getElementPositionLeft(element) {
        // can't use jquery position or offset, because they return values in virtual coordinate system
        // on high dpi devices such as smartphones, and we need CSS pixels coords
        return parseInt(element.css("left"), 10);
    }

    var levelsParameters = [{
        speedMin: 10000,
        speedMax: 15000,
        density: 4
    },
    {
        speedMin: 7500,
        speedMax: 12500,
        density: 8
    },
    {
        speedMin: 5000,
        speedMax: 10000,
        density: 12
    }];
    var currentLevel = 0;

    presenter.ERROR_CODES = {
        I01: 'Property Image cannot be empty',
        D01: 'Description too long. Max is 20 characters',
        L01: 'Property level cannot be empty',
        L02: 'Property level fill with numbers in range 1 - 3',
        P01: 'Property Points to Finish expects number',
        O01: 'Property Items cannot be empty',
        W01: 'Property Width or Height cannot be empty'
    };

    function parseItems (rawItems) {
        var result = [];

        if (ModelValidationUtils.isArrayEmpty(rawItems)) {
            return getErrorObject('O01');
        }

        for (var i=0; i&lt;rawItems.length; i++) {
            var rawItem = rawItems[i];

            var image = rawItem['Image'];
            var description = rawItem['Description'];
            var isCorrect = ModelValidationUtils.validateBoolean(rawItem['Is Correct']);
            var level = rawItem['Level'];

            if (ModelValidationUtils.isStringWithPrefixEmpty(image, "/file/")) {
                return getErrorObject('I01');
            }

            if (description.length &gt; 20) {
                return getErrorObject('D01');
            }

            if (!level) {
                return getErrorObject('L01');
            }

            level = level.split(',').filter(function (l) {
                return l !== '';
            }).map(function (l) {
                return parseInt(l.trim(), 10);
            });

            // remove duplicates
            level = level.filter(function(item, pos) {
                return level.indexOf(item) === pos;
            });

            var isCorrectNumbersInLevels = level.every(function (l) {
                return l === 1 || l === 2 || l === 3;
            });
            if (!isCorrectNumbersInLevels) {
                return getErrorObject('L02');
            }

            result.push(getItemObject(i, image, description, isCorrect, level));
        }

        return getCorrectObject(result);
    }

    function parsePointsToFinish (pointsRaw) {
        var points = parseInt(pointsRaw || 0, 10);

        if (isNaN(points)) {
            return getErrorObject('P01');
        }

        return getCorrectObject(points);
    }

    function parseWidthHeight (propertyValue) {
        var value = parseInt(propertyValue, 10);

        if (isNaN(value)) {
            return getErrorObject('W01');
        }

        return getCorrectObject(value);
    }

    presenter.calculateLevelsItems = function (items) {
        var result = [[], [], []];

        for (var i=0; i&lt;items.length; i++) {
            var item = items[i];

            for (var j=0; j&lt;item.levels.length; j++) {
                result[item.levels[j]-1].push(item);
            }
        }

        return result;
    };

    presenter.validateModel = function (model) {
        var validatedItems = parseItems(model['Items']);
        if (!validatedItems.isValid) {
            return getErrorObject(validatedItems.errorCode);
        }

        var validatedPointsToFinish = parsePointsToFinish(model['Points to finish']);
        if (!validatedPointsToFinish.isValid) {
            return getErrorObject(validatedPointsToFinish.errorCode);
        }

        var validatedItemWidth = parseWidthHeight(model['Item_Width']);
        if (!validatedItemWidth.isValid) {
            return getErrorObject(validatedItemWidth.errorCode);
        }

        var validatedItemHeight = parseWidthHeight(model['Item_Height']);
        if (!validatedItemHeight.isValid) {
            return getErrorObject(validatedItemHeight.errorCode);
        }

        return {
            items: validatedItems.value,
            plateImage: model['Plate image'],
            levelsItems: presenter.calculateLevelsItems(validatedItems.value),
            pointsToFinish: validatedPointsToFinish.value,
            countErrors: ModelValidationUtils.validateBoolean(model["Count errors"]),
            itemWidth: validatedItemWidth.value,
            itemHeight: validatedItemHeight.value,
            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isValid: true
        }
    };

    presenter.onDestroy = function () {
        presenter.clearCatchObjects(objects);

        objects = null;
    };

    presenter.clearCatchObjects = function (objects) {
        objects.forEach( function(value) {
            value.obj.stop();
            value.obj.remove();
            value.obj = null;
        });
    };

    function makePlate () {
        var plateImage = presenter.configuration.plateImage !== "" &amp;&amp; presenter.configuration.plateImage !== undefined ? presenter.configuration.plateImage : getImageUrlFromResources('plate.png');

        $plateElement = $('&lt;img class="plate" /&gt;');
        $plateElement.attr('src', plateImage);

        presenter.$view.append($plateElement);

        // put the plate in the center of screen
        var addOnWidth = presenter.$view.width();
        var centerPos = addOnWidth/2 - $plateElement.width()/2;
        $plateElement.css('left', centerPos + 'px' );
    }

    function makeDescription (description) {
        var $description = $('&lt;span class="description"&gt;' + description + '&lt;/span&gt;');
        $description.css('top', '-15px');
        return $description;
    }

    function setDescriptionPosition (description, itemWidth) {
        var descWidth = description.outerWidth(); // get full width, including padding
        var leftPos= (-descWidth/2 + itemWidth/2) + 'px';
        description.css('left', leftPos );
    }

    function getRandomItemFromLevel (level) {
        var itemsForLevel = presenter.configuration.levelsItems[level];
        return itemsForLevel[Math.floor(Math.random() * itemsForLevel.length)];
    }

    function onNewPoint (itemNumber) {
        points++;
        sendEvent(itemNumber, 1, true);

        var isInfiniteGame = presenter.configuration.pointsToFinish === 0;
        var score = presenter.configuration.countErrors ? (points - errors) : points;

        if (!isInfiniteGame &amp;&amp; score &gt;= presenter.configuration.pointsToFinish) {
            sendEvent('all', 'EOG', true);
            presenter.reset(true);
        }
    }

    function onNewError (itemNumber) {
        errors++;
        sendEvent(itemNumber, 1, false);
    }

    function reCreateFallingObject (itemNumber) {
        sendEvent(itemNumber, 0, false);

        makeFallingObject(0);
    }

    function makeFallingObject (yOffset) {
        if (isGameOver) {
            return;
        }

        var addOnHeight = presenter.$view.height();
        var itemObject = getRandomItemFromLevel(currentLevel);

        var $objectElement = $('&lt;div class="fallingObject"&gt;&lt;/div&gt;');
        $objectElement.css('background', 'url(' + itemObject.image + ')');
        var description = makeDescription(itemObject.description);
        $objectElement.append(description);

        $objectElement.css('background-size', 'cover');
        presenter.$view.append($objectElement);

        setDescriptionPosition(description, presenter.configuration.itemWidth); // we have to do this after appending object to DOM

        var longestWidth = Math.max(description.outerWidth(), presenter.configuration.itemWidth);
        var xPosition = getRandomInt(longestWidth/2, presenter.$view.width() - longestWidth);

        $objectElement.css('left', xPosition + 'px');
        $objectElement.css('top', '-' + (100 + yOffset) + 'px');
        $objectElement.css('width', presenter.configuration.itemWidth);
        $objectElement.css('height', presenter.configuration.itemHeight);

        var duration = getRandomInt(levelsParameters[currentLevel].speedMin, levelsParameters[currentLevel].speedMax);
        var landingPosition = (addOnHeight + 10) + 'px';
        var initialTimeInMs = new Date().getTime();

        startMoving($objectElement, landingPosition, duration, itemObject, xPosition, initialTimeInMs);
    }

    function removeObject(object) {
        object.stop();
        object.remove();
        objects = objects.filter( function (objectIter) {
                return objectIter.obj !== object;
            }
        );
    }

    function startMoving($objectElement, landingPosition, duration, itemObject, xPosition, initialTime) {
        var isRemoved = false;

        objects.push( { obj: $objectElement, duration: duration, landing: landingPosition, item: itemObject, xPos: xPosition, initialTime: initialTime } );

        $objectElement.animate({'top': landingPosition}, {
            duration: duration,
            easing: 'linear',
            complete: function () {
                removeObject($objectElement);
                reCreateFallingObject(itemObject.index + 1);
            },
            step: function (now, tween) {
                if (isRemoved) return;

                var addOnHeight = presenter.$view.height();
                now = Math.round(now);
                var elementBotYPosition = now + $objectElement.height();
                var isInCatchLevel = elementBotYPosition &lt; addOnHeight &amp;&amp; elementBotYPosition &gt; addOnHeight - $plateElement.height();

                if (isInCatchLevel) {
                    var elementLeftEdge = xPosition;
                    var elementRightEdge = elementLeftEdge + $objectElement.width();

                    var plateLeftEdge = getElementPositionLeft($plateElement);
                    var plateRightEdge = plateLeftEdge + $plateElement.width();

                    if (!(plateLeftEdge &gt; elementRightEdge || plateRightEdge &lt; elementLeftEdge)) {
                        if (itemObject.isCorrect) {
                            onNewPoint(itemObject.index + 1);
                        } else {
                            onNewError(itemObject.index + 1);
                        }

                        removeObject($objectElement);
                        makeFallingObject(0);
                        isRemoved = true;
                    }
                }
            }
        });

    }

    function startGame (level) {
        makePlate();

        var numberOfElements = levelsParameters[level].density;
        for (var i=0; i&lt;numberOfElements; i++) {
            makeFallingObject(150 * i);
        }
    }

    function movePlate (isDirectionToRight) {

        if (isPaused) {
            return;
        }

        var addOnPositionLeft = getElementPositionLeft(presenter.$view);
        var addOnWidth = presenter.$view.width();

        var platePositionLeft = addOnPositionLeft + getElementPositionLeft($plateElement);
        var plateWidth = $plateElement.width();

        var isPositionZeroLeft = platePositionLeft &lt;= addOnPositionLeft;
        var isPositionZeroRight = platePositionLeft + plateWidth &gt;= addOnPositionLeft + addOnWidth;

        if (!isDirectionToRight &amp;&amp; isPositionZeroLeft || isDirectionToRight &amp;&amp; isPositionZeroRight) {
            return;
        }

        var hasSpaceLeft = !isDirectionToRight &amp;&amp; (platePositionLeft - addOnPositionLeft &lt; plateWidth);
        var hasSpaceRight = isDirectionToRight &amp;&amp; (platePositionLeft + plateWidth + plateWidth &gt;= addOnPositionLeft + addOnWidth);

        var options = {
            duration: 'fast',
            easing: 'swing',
            queue: false
        };

        if (hasSpaceLeft) {
            $plateElement.animate({
                left: '0px'
            }, options);
        } else if (hasSpaceRight) {
            $plateElement.animate({
                left: (addOnWidth - plateWidth) + 'px'
            }, options);
        } else {
            var directionSign = isDirectionToRight ? '+' : '-';
            $plateElement.animate({
                left: directionSign + '=[plateWidth]px'.replace('[plateWidth]', plateWidth)
            }, options);
        }
    }

    function turnOnEventListeners () {
        presenter.$view.keydown(function (e) {
            if (e.key.localeCompare('ArrowLeft') === 0 || e.key.localeCompare('Left') === 0) {
                movePlate(false);
            }
            if (e.key.localeCompare('ArrowRight') === 0 || e.key.localeCompare('Right') === 0) {
                movePlate(true);
            }
        });

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            presenter.$view.on('touchstart', function (e) {
                e.preventDefault();

                const posX = e.originalEvent.touches[0].pageX;
                const scaledX = posX / getScale().X;

                if (!isAbovePlateCenter(scaledX)) {
                    const isLeftSide = isPointOnLeftSide(scaledX);
                    movePlate(isLeftSide);
                }
            });
        } else {
            presenter.$view.on('click', function (e) {
                e.preventDefault();

                const posX = e.clientX;
                const scaledX = posX / getScale().X;

                if (!isAbovePlateCenter(scaledX)) {
                    const isLeftSide = isPointOnLeftSide(scaledX);
                    movePlate(isLeftSide);
                }
            });
        }

        presenter.$view.focus();
    }

    function getScale() {
        if (presenter.playerController) {
            const scaleInformation = presenter.playerController.getScaleInformation();
            if (scaleInformation.baseScaleX !== 1.0 ||
                scaleInformation.baseScaleY !== 1.0 ||
                scaleInformation.scaleX !== 1.0 ||
                scaleInformation.scaleY !== 1.0
            ) {
                return {X: scaleInformation.scaleX, Y: scaleInformation.scaleY};
            }
        }

        const $content = $("#content");
        if ($content.size() &gt; 0) {
            const contentElem = $content[0];
            const scaleX = contentElem.getBoundingClientRect().width / contentElem.offsetWidth;
            const scaleY = contentElem.getBoundingClientRect().height / contentElem.offsetHeight;
            return {X: scaleX, Y: scaleY};
        } else {
            return {X: 1.0, Y: 1.0};
        }
    }

    function turnOffEventListeners () {
        presenter.$view.off();
    }

    function isAbovePlateCenter(point) {
        var addonBounds = presenter.$view[0].getBoundingClientRect();
        var addonLeftPos = addonBounds.left;
        var relativePoint = point - addonLeftPos;
        var quarterWidth =  Math.round($plateElement.width() / 4);
        var plateLeft = getElementPositionLeft($plateElement) + quarterWidth;
        var plateRight = plateLeft + quarterWidth*2;
        return relativePoint &gt; plateLeft &amp;&amp; relativePoint &lt; plateRight;
    }

    function isPointOnLeftSide(point) {
        // we have to obtain the virtual coordinates of addon, because point from touchstart
        // will come in virtual coords, in case of high dpi devices such as smartphones
        // getBoundingClientRect will work correctly in css pixels in case of low dpi devices
        var addonBounds = presenter.$view[0].getBoundingClientRect();
        var addonLeftPos = addonBounds.left / getScale().X;
        var plateCenterPos = getElementPositionLeft($plateElement) + Math.round($plateElement.width() / 2);
        return (point - addonLeftPos) &gt; plateCenterPos;
    }

    function makeWelcomePage () {
        var $welcomePage = $('&lt;div class="welcome"&gt;&lt;/div&gt;');
        $welcomePage.css('background', 'url(' + getImageUrlFromResources('startGame.png') + ') no-repeat');
        $welcomePage.css('background-size', 'auto');
        $welcomePage.css('background-position-x', 'center');
        $welcomePage.css('background-position-y', 'center');

        return $welcomePage;
    }

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        presenter.$view = $(view);
        presenter.$view.attr('tabindex', 1);

        if (!isPreview) {
            var $welcomePage = makeWelcomePage();
            presenter.$view.append($welcomePage);

            $welcomePage.on('click', function () {
                if (isPaused) {
                    return;
                }
                startGame(currentLevel);
                turnOnEventListeners();
                $welcomePage.remove();
            });

        }
    };

    function stopAndRemoveFallingObjects () {
        presenter.$view.find('.fallingObject').each(function () {
            $(this).stop();
            $(this).remove();
        });
        presenter.$view.empty();
    }

    presenter.setLevel = function (level) {
        level = parseInt(level, 10);
        if (level === 1 || level === 2 || level === 3) {
            currentLevel = level-1;
            turnOffEventListeners();
            stopAndRemoveFallingObjects();
            startGame(currentLevel);
            turnOnEventListeners();
        }
    };

    presenter.getPoints = function () {
        return points;
    };

    presenter.getErrors = function () {
        return errors;
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.reset = function (isEndGame) {
        isEndGame = isEndGame || false;
        isGameOver = isEndGame;

        if (!isEndGame) {
            points = 0;
            errors = 0;
            currentLevel = 0;
        }
        isPaused = false;
        objects = [];

        turnOffEventListeners();
        stopAndRemoveFallingObjects();

        if (!isEndGame) {
            var $welcomePage = makeWelcomePage();
            presenter.$view.append($welcomePage);

            $welcomePage.on('click', function () {
                if (isPaused) {
                    return;
                }
                startGame(currentLevel);
                turnOnEventListeners();
                $welcomePage.remove();
            });
        }
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function (name, params) {
        if (!presenter.configuration.isValid) {
            return;
        }

        var commands = {
            'reset': presenter.reset,
            'show': presenter.show,
            'hide': presenter.hide,

            'setLevel': presenter.setLevel,
            'getPoints': presenter.getPoints,
            'getErrors': presenter.getErrors
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getErrorCount = function () {
        return errors;
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.pointsToFinish === 0)
            return 0;
        else
            return presenter.configuration.pointsToFinish;
    };

    presenter.getScore = function () {
        return presenter.configuration.countErrors ? Math.max((points - errors), 0) : points;
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.$view.css('visibility') === 'visible',
            points: points,
            errors: errors
        });
    };

    presenter.setState = function (state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        const parsedState = JSON.parse(state);
        points = parsedState.points;
        errors = parsedState.errors;

        presenter.setVisibility(parsedState.isVisible);
    };

    presenter.isAllOk = function () {
        return errors === 0;
    };

    function sendEvent (item, value, isCorrect) {
        function createEventObject(_item, _value, _isCorrect) {
            return {
                'source': presenter.configuration.ID,
                'item': '' + _item,
                'value': '' + _value,
                'score': _isCorrect ? '1' : '0'
            };
        }

        presenter.eventBus.sendEvent('ValueChanged', createEventObject(item, value, isCorrect));
    }

    presenter.setEventBus = function(eventBus) {
        presenter.eventBus = eventBus;

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    function getImageUrlFromResources (fileName) {
        return presenter.playerController.getStaticFilesPath() + 'addons/resources/' + fileName;
    }

    presenter.setWorkMode = function () {
        resumeSimulation();
    };

    presenter.setShowErrorsMode = function () {
        pauseSimulation();
    };

    presenter.showAnswers = function () {
        pauseSimulation();
    };

    presenter.hideAnswers = function () {
        resumeSimulation();
    };

    presenter.onEventReceived = function (eventName) {

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    function pauseSimulation() {
        if (isPaused)
            return;
        isPaused = true;
        pausedTimeInMs = new Date().getTime();

        for (var i = 0; i &lt; objects.length; i++) {
            var object = objects[i];
            object.obj.clearQueue();
            object.obj.stop();
        }
    }

    function resumeSimulation() {
        if (!isPaused)
            return;
        isPaused = false;
        // copy objects to new array, because it will be edited in startMoving
        var newObjects = objects.slice();
        objects = [];
        var currentTimeInMs = new Date().getTime();

        for (var j = 0; j &lt; newObjects.length; j++) {
            var obj = newObjects[j];

            // rescale animations
            var dt = obj.duration - (pausedTimeInMs - obj.initialTime);
            startMoving(obj.obj, obj.landing, dt, obj.item, obj.xPos, currentTimeInMs);
        }
    }

    return presenter;
}

AddonCatch_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Clock" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="TimeStandard" nameLabel="Clock_property_time_standard" type="{24H,12H}"/>
		<property name="InitialTime" nameLabel="Clock_property_initial_time" type="string"/>
		<property displayName="Step (minutes)" name="TimeStep" nameLabel="Clock_property_time_step" type="string"/>
		<property name="ShowClockLabels" nameLabel="Clock_property_show_clock_labels" type="boolean"/>
		<property name="ShowSecondHand" nameLabel="Clock_property_show_second_hand" type="boolean"/>
		<property displayName="Step (seconds)" name="SecondTimeStep" nameLabel="Clock_property_second_time_step" type="string"/>
		<property name="ActiveHand" nameLabel="Clock_property_active_hand" type="{Hour, Minute, Second, H+M, H+S, M+S, All}"/>
		<property name="SynchronizeHands" nameLabel="Clock_property_synchronize_hands" type="{None, Min with Hour, Both, All}"/>
		<property name="Images" nameLabel="Clock_property_images" type="list">
			<property name="Background" nameLabel="Clock_property_background" type="image"/>
		</property>
		<property name="CorrectAnswer" nameLabel="Clock_property_correct_answer" type="string"/>
		<property name="isDisable" nameLabel="Clock_property_is_disable" type="boolean"/>
		<property name="isActivity" nameLabel="Clock_property_is_activity" type="boolean"/>
        <property name="Continuous events" nameLabel="Clock_property_continuous_events" type="boolean"/>
	</model>
<css>#clock-face {
    stroke: black;
    stroke-width: 2px;
    fill: white;
}

.analog-clock {
    border: 3px solid transparent;
    -ms-touch-action: none;
}

.analog-clock.incorrect {
    border: 3px solid #ff4901;
}

.analog-clock.correct {
    border: 3px solid #06d401;
}

#h-hand,#m-hand, #s-hand {
    stroke: #A52A2A;
    stroke-opacity: 0.0;
}

#h-hand {
    stroke-width: 50px
}

#m-hand {
    stroke-width: 50px;
}

#s-hand {
    stroke-width: 50px;
}

.time-text {
    text-align: center;
}

.text_label { /*fill: blue;*/

}

#label_mask {
    opacity: 0;
}
</css><view>&lt;p class="clock-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id="analog-clock" class="analog-clock"&gt;&lt;/div&gt;
</view><preview>&lt;p class="clock-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id="analog-clock" class="analog-clock"&gt;&lt;/div&gt;
</preview><presenter>function AddonClock_create() {
    var presenter = function() {
    };

    presenter.currentHand = 0;
    presenter.getHour = 0;
    presenter.getMinute = 0;
    presenter.mouseX = 0;
    presenter.mouseY = 0;
    presenter.isHandInMove = false;
    presenter.currentHour = 0;
    presenter.currentMinute = 0;
    presenter.lastMinuteValue = 0;
    presenter.Step = 1;
    presenter.StepSeconds = 1;
    presenter.showClockLabels = false;
    presenter.isActivity = false;
    presenter.isErrorCheckingMode = false;
    presenter.TimeStandard = 24;
    presenter.showAnswersMode = false;
    presenter.shouldSendEventTime = null;
    presenter.isGradualShowAnswersActive = false;
    presenter.showSecondHand = false;

    function displayText() {
        var textToDisplay = presenter.model['Text to be displayed'], isTextColored = presenter.model['Color text'] === 'True', $textContainer = presenter.$view
            .find('.some-text-container');

        $textContainer.text(textToDisplay);
        if (isTextColored) {
            $textContainer.css('color', 'red');
        }
    }




    presenter.executeCommand = function(name, params) {
        switch (name.toLowerCase()) {
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'setClockTime'.toLowerCase():
                presenter.setClockTime(params[0]);
                break;
            case 'getCurrentTime'.toLowerCase():
                presenter.getCurrentTime();
                break;
            case 'getCurrentHour'.toLowerCase():
                presenter.getCurrentHour();
                break;
            case 'getCurrentMinute'.toLowerCase():
                presenter.getCurrentMinute();
                break;
            case 'getCurrentSeconds'.toLowerCase():
                presenter.getCurrentSeconds();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'markAsCorrect'.toLowerCase():
                presenter.markAsCorrect();
                break;
            case 'markAsWrong'.toLowerCase():
                presenter.markAsWrong();
                break;
            case 'markAsEmpty'.toLowerCase():
                presenter.markAsEmpty();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'isDisabled'.toLowerCase():
                presenter.isDisabled();
                break;
        }
    };

    presenter.drawClock = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.modelID = model.ID;
        presenter.Height = parseInt(model.Height, 10);
        presenter.Width = parseInt(model.Width, 10);

        var centerX = parseInt(presenter.Height / 2, 10);
        var centerY = parseInt(presenter.Width / 2, 10);
        presenter.center = centerX &gt; centerY ? centerY : centerX;
        presenter.radius = presenter.center - 5;

        var size = 2 * presenter.center;

        presenter.$view.find('.analog-clock').css({
            'width' : size,
            'height' : size
        });

        var vector = presenter.center / 15;

        var fig = '&lt;svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="'
            + presenter.Width + '" height="' + presenter.Height + '"&gt;';
        var uniqueID = Math.floor((Math.random() * 10000) + 1);
        if (model.Images[0].Background != "") {
            fig += '&lt;defs&gt;';
            fig += '&lt;pattern id="imageBackground' + presenter.modelID
                + uniqueID
                + '" x="0" y="0" patternUnits="userSpaceOnUse" height="'
                + (2 * presenter.center) + '" width="'
                + (2 * presenter.center) + '"&gt;';
            fig += '&lt;image y="0" x="0" id="imageBackground' + presenter.modelID
                + uniqueID + '" xlink:href="' + model.Images[0].Background
                + '" height="' + (2 * presenter.center) + '" width="'
                + (2 * presenter.center) + '" /&gt;';
            fig += '&lt;/pattern&gt;';
            fig += '&lt;/defs&gt;';

            fig += '&lt;circle id="clock-face-with-image" class="circle" cx="'
                + presenter.center + '" cy="' + presenter.center + '" r="'
                + presenter.radius + '" fill="url(#imageBackground'
                + presenter.modelID + uniqueID + ')"/&gt;';
        } else {
            fig += '&lt;circle id="clock-face" class="circle" cx="'
                + presenter.center + '" cy="' + presenter.center + '" r="'
                + presenter.radius + '" /&gt;';
        }
        if (presenter.showClockLabels) {
            fig += '&lt;text class="text_label" x="'
                + (presenter.center - 17 * vector / 10) + '" y="'
                + (presenter.center / 5 + vector / 2) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;12&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (2 * presenter.center - presenter.center / 5) + '" y="'
                + (presenter.center + presenter.center / 10 - vector / 2)
                + '" font-size="' + (presenter.center / 5)
                + 'px" &gt;3&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (presenter.center - presenter.center / 20) + '" y="'
                + (2 * presenter.center - presenter.center / 10)
                + '" font-size="' + (presenter.center / 5)
                + 'px" &gt;6&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="' + (presenter.center / 10)
                + '" y="'
                + (presenter.center + presenter.center / 10 - vector / 2)
                + '" font-size="' + (presenter.center / 5)
                + 'px" &gt;9&lt;/text&gt;';

            fig += '&lt;text class="text_label" x="'
                + (14 * presenter.center / 10) + '" y="'
                + (9 * presenter.center / 25) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;1&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (4 * presenter.center / 10 + vector) + '" y="'
                + (9 * presenter.center / 25) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;11&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (8.4 * presenter.center / 5) + '" y="'
                + (7 * presenter.center / 10 - vector) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;2&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (presenter.center / 5 - vector / 2) + '" y="'
                + (7 * presenter.center / 10 - vector) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;10&lt;/text&gt;';

            fig += '&lt;text class="text_label" x="'
                + (14 * presenter.center / 10 - vector) + '" y="'
                + (44 * presenter.center / 25 + vector) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;5&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (4 * presenter.center / 10 + 2 * vector) + '" y="'
                + (44 * presenter.center / 25 + vector) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;7&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="'
                + (8.4 * presenter.center / 5) + '" y="'
                + (15 * presenter.center / 10) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;4&lt;/text&gt;';
            fig += '&lt;text class="text_label" x="' + (presenter.center / 5)
                + '" y="' + (15 * presenter.center / 10) + '" font-size="'
                + (presenter.center / 5) + 'px" &gt;8&lt;/text&gt;';
            fig += '&lt;rect id="label_mask" x="0" y="0" width="'
                + (2 * presenter.center) + '" height="'
                + (2 * presenter.center) + '"/&gt;';
        }

        if (presenter.showSecondHand) {
            if (presenter.isMinuteHandActive()){
                fig += presenter.getSecondHandElement(vector);
                fig += presenter.getHourHandElement(vector);
                fig += presenter.getMinuteHandElement(vector);
            } else {
                fig += presenter.getMinuteHandElement(vector);
                fig += presenter.getSecondHandElement(vector);
                fig += presenter.getHourHandElement(vector);
            }
        } else {
            if (presenter.ActiveHand == 'HourHand' || presenter.ActiveHand == 'Hour') {
                fig += presenter.getMinuteHandElement(vector);
                fig += presenter.getHourHandElement(vector);
            } else {
                fig += presenter.getHourHandElement(vector);
                fig += presenter.getMinuteHandElement(vector);
            }
        }

        fig += '&lt;/svg&gt;';

        return fig;
    };

    presenter.getHourHandElement = function (vector) {
        return '&lt;rect id="h-hand" x="' + (presenter.center - vector)
                + '" y="' + (presenter.center / 2) + '" ry="'
                + (2 * vector) + '" rx="' + (2 * vector) + '" width="'
                + (2 * vector) + '" height="'
                + (presenter.center / 2 + vector) + '" /&gt;';
    };

    presenter.getMinuteHandElement = function (vector) {
        return '&lt;rect id="m-hand" x="' + (presenter.center - vector)
                + '" y="' + (presenter.center / 5) + '" ry="'
                + (2 * vector) + '" rx="' + (2 * vector) + '" width="'
                + (2 * vector) + '" height="'
                + (4 * presenter.center / 5 + vector) + '" /&gt;';
    };

    presenter.getSecondHandElement = function (vector) {
        return '&lt;rect id="s-hand" x="' + (presenter.center - vector / 4)
                + '" y="' + (presenter.center / 5) + '" ry="'
                + (2 * vector) + '" rx="' + (2 * vector) + '" width="'
                + (vector / 2) + '" height="'
                + (4 * presenter.center / 5 + vector) + '" /&gt;';
    };

    presenter.getNewTime = function(myTime) {
        var newTime = [];

        var position = myTime.indexOf(':');
        newTime[0] = myTime.slice(0, position);
        newTime[1] = myTime.slice(position + 1, myTime.length);
        return newTime;
    };

    presenter.getTimeWithSeconds = function(time) {
        return time.split(":");
    }

    presenter.validateTimeCorrectnessWithSeconds = function (time) {
        let timeSplited = time.split(":");

        if (timeSplited.length !== 3) {
            return false;
        }

        return (
            presenter.checkIfValidNumber(timeSplited[0])
            &amp;&amp; presenter.checkIfValidNumber(timeSplited[1])
            &amp;&amp; presenter.checkIfValidNumber(timeSplited[2])
        );
    }

    presenter.checkTime = function(time) {
        var hour, minute;

        if (time.indexOf(':') !== -1) {
            var position = time.indexOf(':');
            hour = time.slice(0, position);
            minute = time.slice(position + 1, time.length);

            if (presenter.checkIfValidNumber(hour) &amp;&amp; presenter.checkIfValidNumber(minute)) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }

    };

    presenter.checkIfValidNumber = function (num) {
        return parseInt(num, 10) === Math.round(num * 100) / 100 &amp;&amp; parseInt(num, 10) &gt; -1 &amp;&amp; !(isNaN(num));
    }

    presenter.countTimeFromMinuteAngle = function(angleValue) {
        value = angleValue / 6;
        return value;
    };

    presenter.setClockTime = function(time) {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }

        if (presenter.showSecondHand) {
            presenter.setClockTimeWithSeconds(time);
            return;
        }

        if (!presenter.checkTime(time)) {
            return 0;
        }
        var newTime = presenter.getNewTime(time);

        presenter.currentMinuteValue = parseInt(newTime[1], 10);
        presenter.currentHourValue = parseInt(newTime[0], 10)
            % presenter.TimeStandard;
        if (presenter.TimeStandard == 12 &amp;&amp; presenter.currentHourValue == 0) {
            presenter.currentHourValue = 12;
        }
        var h = 30 * ((parseInt(presenter.currentHourValue, 10) % 12) + parseInt(
            presenter.currentMinuteValue, 10) / 60);
        var m = 6 * parseInt(presenter.currentMinuteValue, 10);

        presenter.currentHourAngle = h;

        presenter.setAttr('h-hand', h);
        presenter.setAttr('m-hand', m);

    };

    presenter.setClockTimeWithSeconds = function (time) {
        if (!presenter.validateTimeCorrectnessWithSeconds(time)) {
            console.error("Time to set with seconds is not valid");
            return;
        }

        const newTime = presenter.getTimeWithSeconds(time);
        presenter.currentHourValue = parseInt(newTime[0], 10) % presenter.TimeStandard;
        presenter.currentMinuteValue = parseInt(newTime[1], 10);
        presenter.currentSecondValue = parseInt(newTime[2], 10);

        if (presenter.TimeStandard == 12 &amp;&amp; presenter.currentHourValue == 0) {
            presenter.currentHourValue = 12;
        }

        const h = 30 * (presenter.currentHourValue % 12 + presenter.currentMinuteValue / 60 + presenter.currentSecondValue / 3600);
        const m = 6 * (presenter.currentMinuteValue + presenter.currentSecondValue / 60);
        const s = 6 * (presenter.currentSecondValue);

        presenter.currentHourAngle = h;
        presenter.currentMinuteAngle = m;
        presenter.currentSecondAngle = s;

        presenter.setAttr('h-hand', h);
        presenter.setAttr('m-hand', m);
        presenter.setAttr('s-hand', s);
    }

    presenter.setTimeNotSynhronized = function() {
        let h = presenter.currentHourValue;
        let m = presenter.currentMinuteValue;
        let s = presenter.currentSecondValue;

        if (presenter.currentHand === 'h-hand') {
            presenter.setNewCurrentHourValue();
            h = presenter.currentHourValue;
        } else if (presenter.currentHand === 'm-hand') {
            m = Math.round(presenter.currentHandValue / 6);
            if (m === 60) {
                m = 0;
            }
        } else if (presenter.currentHand === 's-hand') {
            s = Math.round(presenter.currentHandValue / 6);
            if (s === 60) {
                s = 0;
            }
        }

        const timeToSet = presenter.showSecondHand ? `${h}:${m}:${s}` : `${h}:${m}`;
        presenter.setClockTime(timeToSet);

    };

    presenter.setTimeSynhronizedMinWithHour = function() {

        let h = presenter.currentHourValue;
        let m = presenter.currentMinuteValue;
        let s = presenter.currentSecondValue;
        var valueMinutePrevious = presenter.currentMinuteValue;

        if (presenter.currentHand === 'h-hand') {
            presenter.setNewCurrentHourValue();
            h = presenter.currentHourValue;

        } else if (presenter.currentHand === 'm-hand') {
            m = Math.round(presenter.currentHandValue / 6);
            if (m === 60) {
                m = 0;
            }

            presenter.hourValueOnFullPeriod(m, valueMinutePrevious);
            h = presenter.currentHourValue;
            valueMinutePrevious = m;

        } else if (presenter.currentHand === 's-hand') {
            s = Math.round(presenter.currentHandValue / 6);
            if (s === 60) {
                s = 0;
            }
        }

        const timeToSet = presenter.showSecondHand ? `${h}:${m}:${s}` : `${h}:${m}`;
        presenter.setClockTime(timeToSet);

    };

    presenter.setTimeSynhronizedBoth = function() {

        let h = presenter.currentHourValue;
        let m = presenter.currentMinuteValue;
        let s = presenter.currentSecondValue;
        var valueMinutePrevious = presenter.currentMinuteValue;

        if (presenter.currentHand === 'h-hand') {
            presenter.setNewCurrentHourValue();
            h = presenter.currentHourValue;

            m = Math.round(2 * (presenter.currentHandValue % 30));
            m = presenter.countNewHandValue(m, presenter.Step);

        } else if (presenter.currentHand === 'm-hand') {
            m = Math.round(presenter.currentHandValue / 6);
            if (m === 60) {
                m = 0;
            }

            presenter.hourValueOnFullPeriod(m, valueMinutePrevious);
            h = presenter.currentHourValue;
            valueMinutePrevious = m;

        } else if (presenter.currentHand === 's-hand') {
            s = Math.round(presenter.currentHandValue / 6);
            if (s === 60) {
                s = 0;
            }
        }

        const timeToSet = presenter.showSecondHand ? `${h}:${m}:${s}` : `${h}:${m}`;
        presenter.setClockTime(timeToSet);
    };

    presenter.setTimeSynhronizedAll = function() {

        let h = presenter.currentHourValue;
        let m = presenter.currentMinuteValue;
        let s = presenter.currentSecondValue;
        let valueMinutePrevious = presenter.currentMinuteValue;
        let valueSecondPrevious = presenter.currentSecondValue;

        if (presenter.currentHand === 'h-hand') {
            presenter.setNewCurrentHourValue();
            h = presenter.currentHourValue;

            m = Math.round(2 * (presenter.currentHandValue % 30));
            m = presenter.countNewHandValue(m, presenter.Step);

        } else if (presenter.currentHand === 'm-hand') {
            m = Math.round(presenter.currentHandValue / 6);
            if (m === 60) {
                m = 0;
            }

            presenter.hourValueOnFullPeriod(m, valueMinutePrevious);
            h = presenter.currentHourValue;
            valueMinutePrevious = m;

        } else if (presenter.currentHand === 's-hand') {
            s = Math.round(presenter.currentHandValue / 6);
            if (s === 60) {
                s = 0;
            }
            presenter.minuteValueOnFullPeriod(s, valueSecondPrevious);
            m = presenter.currentMinuteValue;
            presenter.hourValueOnFullPeriod(m, valueMinutePrevious);
            h = presenter.currentHourValue;
        }

        const timeToSet = presenter.showSecondHand ? `${h}:${m}:${s}` : `${h}:${m}`;
        presenter.setClockTime(timeToSet);
    };

    presenter.minuteValueOnFullPeriod = function (value, prevValue) {
        if (value &lt; 15 &amp;&amp; prevValue &gt; 45) {
            if (presenter.currentMinuteValue == 59) {
                presenter.currentMinuteValue = 0;
            } else {
                presenter.currentMinuteValue++;
            }
        }
        if (value &gt; 45 &amp;&amp; prevValue &lt; 15) {
            if (presenter.currentMinuteValue == 0) {
                presenter.currentMinuteValue = 59;
            } else {
                presenter.currentMinuteValue--;
            }
        }
    };

    presenter.hourValueOnFullPeriod = function (value, prevValue) {
        if (value &lt; 15 &amp;&amp; prevValue &gt; 45) {
            if (presenter.currentHourValue == 23) {
                presenter.currentHourValue = 0;
            } else {
                presenter.currentHourValue++;
            }
        }
        if (value &gt; 45 &amp;&amp; prevValue &lt; 15) {
            if (presenter.currentHourValue == 0) {
                presenter.currentHourValue = 23;
            } else {
                presenter.currentHourValue--;
            }
        }
    };



    presenter.setNewCurrentHourValue = function () {
        if (presenter.currentHourValue == 11
                &amp;&amp; presenter.currentHandValue &lt; 30) {
                presenter.currentHourValue = 12;
            }
            if (presenter.currentHourValue == 12
                &amp;&amp; presenter.currentHandValue &gt; 330) {
                presenter.currentHourValue = 11;
            }
            if (presenter.currentHourValue == 0
                &amp;&amp; presenter.currentHandValue &gt; 330) {
                presenter.currentHourValue = 23;
            }
            if (presenter.currentHourValue == 23
                &amp;&amp; presenter.currentHandValue &lt; 30) {
                presenter.currentHourValue = 0;
            }
            if (presenter.currentHourValue &gt;= 12) {
                presenter.currentHourValue = 12 + Math
                    .floor(presenter.currentHandValue / 30);
            } else {
                presenter.currentHourValue = Math
                    .floor(presenter.currentHandValue / 30);
            }
    };

    presenter.moveCurrentHand = function(element) {
        presenter.currentHand = element.id;
    };

    presenter.setAttr = function(id, val) {
        var v = 'rotate(' + val + ', ' + presenter.center + ', '
            + presenter.center + ')';
        presenter.$view.find('#' + id).attr('transform', v);

    };

    presenter.validate = function(view, model) {
        presenter.showSecondHand = ModelValidationUtils.validateBoolean(model["ShowSecondHand"]);
        presenter.ActiveHand = model.ActiveHand;

        presenter.$view = $(view);
        presenter.model = model;
        if (model.TimeStandard == '12H') {
            presenter.TimeStandard = 12;
        } else {
            presenter.TimeStandard = 24;
        }
        $counter = $(view).find('.clock-counter');

        if (!presenter.validateStep(model.TimeStep, $counter, "minute")){
            return false;
        }

        if (presenter.showSecondHand) {
            return presenter.validateModelWithSeconds(view, model, $counter);
        } else {
            return presenter.validateStandardModel(view, model, $counter);
        }
    };

    presenter.validateStep = function (rawStep, counterView, stepName) {
        const step = parseInt(rawStep, 10);

        if (rawStep.length == 0) {
            $counter.text(`Fill step-${stepName} value.`);
            return false;
        }

        if (step != Math.round(rawStep * 100) / 100 || isNaN(rawStep)) {
            $counter.text(`Incorrect step-${stepName} value.`);
            return false;
        }

        if (60 % step != 0) {
            $counter.text(`Step-${stepName} should be a divisor of 60.`);
            return false;
        }

        return true;
    }

    presenter.validateModelWithSeconds = function(view, model, $counter) {
        if(!presenter.validateTimeCorrectnessWithSeconds(model.InitialTime)) {
            $counter.text('Put correct InitialTime (hour:minute:second).');
            return false;
        }

        if (!presenter.validateStep(model.SecondTimeStep, $counter, "second")) {
            return false;
        }

        let time = presenter.getTimeWithSeconds(model.InitialTime);
        let hour = parseInt(time[0], 10);
        let min = parseInt(time[1], 10);
        let sec = parseInt(time[2], 10);
        let timeStandard = parseInt(presenter.TimeStandard, 10);
        const timeStepMinutes = parseInt(model.TimeStep, 10);
        const timeStepSeconds = parseInt(model.SecondTimeStep, 10);

        if (!presenter.validateRangeTime(hour, min, timeStandard)) {
            $counter.text(`Put correct InitialTime (hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`);
            return false;
        }

        if (!presenter.validateHourExceedTimeStandard(hour, timeStandard)) {
            $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`);
            return false;
        }

        if (sec &lt; 0 || sec &gt;= 60) {
            $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`);
            return false;
        }

        if (!presenter.validateIfValueMultipleOfStep(sec, timeStepSeconds)) {
            $counter.text('Value of seconds in InitialTime should be a multiple value of step-second.');
            return false;
        }

        if (!presenter.validateIfValueMultipleOfStep(min, timeStepMinutes)) {
            $counter.text('Value of minutes in InitialTime should be a multiple value of step-minute.');
            return false;
        }

        if (model.isActivity == "True") {
            if (!(presenter.validateTimeCorrectnessWithSeconds(model.CorrectAnswer))) {
                $counter.text('Put CorrectAnswer (hour:minute:second) or uncheck isActivity.');
                return false;
            }

            time = presenter.getTimeWithSeconds(model.CorrectAnswer);
            hour = parseInt(time[0], 10);
            min = parseInt(time[1], 10);
            sec = parseInt(time[2], 10);

            if (!presenter.validateRangeTime(hour, min, timeStandard)) {
                $counter.text(`Put correct InitialTime (hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`)
                return false;
            }

            if (!presenter.validateHourExceedTimeStandard(hour, timeStandard)) {
                $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`)
                return false;
            }

            if (sec &lt; 0 || sec &gt;= 60) {
                $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1}, seconds and minutes in range &lt;0 to 60)).`)
                return false
            }

            if (!presenter.validateIfValueMultipleOfStep(sec, timeStepSeconds)) {
                $counter.text('Value of seconds in CorrectAnswer should be a multiple value of step-seconds.');
                return false;
            }

            if (!presenter.validateIfValueMultipleOfStep(min, timeStepMinutes)) {
                $counter.text('Value of minutes in CorrectAnswer should be a multiple value of step-minutes.');
                return false;
            }
        }

        presenter.Step = timeStepMinutes;
        presenter.StepSeconds = timeStepSeconds;
        presenter.continousEvents = ModelValidationUtils.validateBoolean(model['Continuous events']);

        return true;
    }

    presenter.validateStandardModel = function(view, model, $counter) {
        if (!presenter.checkTime(model.InitialTime)) {
            $counter.text('Put correct InitialTime (hour:minute).');
            return false;
        }

        let getTime = presenter.getNewTime(model.InitialTime);
        let min = parseInt(getTime[1], 10);
        let hour = parseInt(getTime[0], 10);
        let timeStandard = parseInt(presenter.TimeStandard, 10);
        const timeStep = parseInt(model.TimeStep, 10);

        if (!presenter.validateRangeTime(hour, min, timeStandard)) {
            $counter.text(`Put correct InitialTime (hour &lt; ${timeStandard + 1} and minute &lt; 60.`)
            return false;
        }

        if (!presenter.validateHourExceedTimeStandard(hour, timeStandard)) {
            $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1} and minute &lt; 60).`)
            return false;
        }

        if (!presenter.validateIfValueMultipleOfStep(min, timeStep)) {
            $counter.text('Value of minutes in InitialTime should be a multiple value of step.');
            return false;
        }

        if (presenter.isSecondHandActive()) {
            $counter.text('Active hand cannot be H+S, M+S, Second or All because Show Second Hand option is off.');
            return false;
        }

        if (model.isActivity == "True") {
            if (!(presenter.checkTime(model.CorrectAnswer))) {
                $counter.text('Put CorrectAnswer (hour:minute) or uncheck isActivity.');
                return false;
            }

            getTime = presenter.getNewTime(model.CorrectAnswer);
            min = parseInt(getTime[1], 10);
            hour = parseInt(getTime[0], 10);

            if (!presenter.validateRangeTime(hour, min, timeStandard)) {
                $counter.text(`Put correct InitialTime (hour &lt; ${timeStandard + 1} and minute &lt; 60.`)
                return false;
            }

            if (!presenter.validateHourExceedTimeStandard(hour, timeStandard)) {
                $counter.text(`Put correct InitialTime (0 &lt; hour &lt; ${timeStandard + 1} and minute &lt; 60).`)
                return false;
            }

            if (!presenter.validateIfValueMultipleOfStep(min, timeStep)) {
                $counter.text('Value of minutes in CorrectAnswer should be a multiple value of step.');
                return false;
            }

        }

        presenter.Step = timeStep;
        presenter.continousEvents = ModelValidationUtils.validateBoolean(model['Continuous events']);

        return true;
    };

    presenter.validateRangeTime = function (hour, min, timeStandard) {
            if (min &gt; 59 || hour &gt; timeStandard) {
                return false;
            }
            return true;
    };

     presenter.validateHourExceedTimeStandard = function (hour, timeStandard) {
            if (timeStandard == 12 &amp;&amp; hour == 0) {
                return false;
            }
            return true;
    };

     presenter.validateIfValueMultipleOfStep = function (value, step) {
         return value % step === 0;
     };

    presenter.validateTime = function(time) {
        if (presenter.showSecondHand) {
            const myTime = presenter.getTimeWithSeconds(time);
            let h = myTime[0];
            let m = presenter.getValueWithLeadingZero(parseInt(myTime[1], 10));
            let s = presenter.getValueWithLeadingZero(parseInt(myTime[2], 10));
            return `${h}:${m}:${s}`
        }

        var hour, minute, newTime;

        var position = time.indexOf(':');
        hour = time.slice(0, position);
        minute = time.slice(position + 1, time.length);

        hour = parseInt(hour, 10);
        minute = minute &gt; 9 ? parseInt(minute, 10) : "0" + parseInt(minute, 10);
        newTime = hour +":" + minute;
        return newTime;
    };

    presenter.init = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.modelID = model.ID;
        presenter.isActivity = model.isActivity == "True" ? true : false;
        presenter.CorrectAnswer = presenter.validateTime(model.CorrectAnswer);
        presenter.InitialTime = presenter.validateTime(model.InitialTime);
        presenter.isDisable = model.isDisable == "True" ? true : false;
        presenter.wasDisable = model.isDisable == "True" ? true : false;
        presenter.wasVisible = model["Is Visible"] == 'True';
        presenter.isVisible = model["Is Visible"] == 'True';
        presenter.ActiveHand = model.ActiveHand;

        if (model.ShowClockLabels == 'True') {
            presenter.showClockLabels = true;
        }

        presenter.isSynhronized = model.SynchronizeHands;
        var myDiv = $(view).find('.analog-clock')[0];

        var figureClock = presenter.drawClock(view, model);
        $(myDiv).append(figureClock);

        if (presenter.isDisable) {
            $(myDiv).addClass('disable');
        }
    };

    presenter.run = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        if (presenter.validate(view, model)) {

            presenter.init(view, model);
            presenter.setVisibility(presenter.isVisible);

            presenter.setClockTime(model.InitialTime);

            ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'].forEach(event =&gt; {
                presenter.eventBus.addEventListener(event, this);
            });

            jQuery(function($) {
                $(view).find('#analog-clock').mousemove(function(e) {

                    e.stopImmediatePropagation();
                    presenter.currentHand = 0;
                    e.stopPropagation();

                    if (presenter.isHandInMove &amp;&amp; !presenter.isDisable 	&amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.continousEvents) {
                        presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1 : 0);
                    }
                    presenter.isHandInMove = false;
                }).click(function(e) {


                    }).mouseleave(function(e) {
                        if(presenter.isHandInMove){
                            presenter.isHandInMove = false;
                            presenter.currentHand = 0;
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1 : 0);

                        }
                    }).mouseup(function(e) {

                    });
            });

            jQuery(function($) {
                $(view).find('#h-hand')
                    .click(function(e) {
                        e.stopImmediatePropagation();
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        presenter.isHandInMove = false;
                    })
                    .mouseup(
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();

                        if (presenter.isHandInMove &amp;&amp; !presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);

                            presenter.isHandInMove = false;
                        }
                        return false;
                    })
                    .mousedown(
                    function(e) {
                        e.stopImmediatePropagation();
                        if (presenter.isHourHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                            e.stopPropagation();
                            presenter.moveCurrentHand(this);
                            presenter.isHandInMove = true;
                        }
                        return false;
                    })
                    .mousemove(
                    function(e) {
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'mouse');
                            presenter.setTimeBasedOnSyncOption();
                            e.stopPropagation();
                            if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                                presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            }
                            presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }

                    }).mouseleave(function() {

                    });
            });

            jQuery(function($) {
                $(view)
                    .find('#m-hand')
                    .click(function(e) {
                        e.stopImmediatePropagation();
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        presenter.isHandInMove = false;

                    })
                    .mouseup(
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        if (presenter.isHandInMove &amp;&amp; !presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            presenter.isHandInMove = false;
                        }
                        return false;
                    })
                    .mousedown(
                    function(e) {
                        e.stopImmediatePropagation();
                        if ( presenter.isMinuteHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                                e.stopPropagation();
                                presenter.moveCurrentHand(this);
                                presenter.isHandInMove = true;
                        }
                        return false;
                    })
                    .mousemove(
                    function(e) {
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'mouse');
                            presenter.setTimeBasedOnSyncOption();
                            e.stopPropagation();

                        if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                        }
                        presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }

                    }).mouseleave(function() {
                    });
            });

            jQuery(function($) {
                $(view)
                    .find('#s-hand')
                    .click(function(e) {
                        e.stopImmediatePropagation();
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        presenter.isHandInMove = false;

                    })
                    .mouseup(
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        if (presenter.isHandInMove &amp;&amp; !presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            presenter.isHandInMove = false;
                        }
                        return false;
                    })
                    .mousedown(
                    function(e) {
                        e.stopImmediatePropagation();
                        if ( presenter.isSecondHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                                e.stopPropagation();
                                presenter.moveCurrentHand(this);
                                presenter.isHandInMove = true;
                        }
                        return false;
                    })
                    .mousemove(
                    function(e) {
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'mouse');
                            presenter.setTimeBasedOnSyncOption();
                            e.stopPropagation();

                        if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                        }
                        presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }

                    }).mouseleave(function() {
                    });
            });

            jQuery(function($) {
                $(view).find('#label_mask')
                    .click(function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        presenter.isHandInMove = false;

                    }).mouseup(function(e) {

                        if(presenter.isHandInMove){

                            presenter.isHandInMove = false;
                            presenter.currentHand = 0;
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1 : 0);
                        }

                    }).mousemove(function(e) {
                        e.stopImmediatePropagation();

                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'mouse');
                            presenter.setTimeBasedOnSyncOption();
                            e.stopPropagation();
                        }

                    });
            });

            jQuery(function($) {
                $(view).find('#analog-clock').on('touchmove', function(e) {
                    presenter.currentHand = 0;
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                    presenter.isHandInMove = false;
                });
            });

            jQuery(function($) {
                $(view).find('#label_mask').on('touchmove', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                });
            });

            jQuery(function($) {
                $(view)
                    .find('#m-hand')
                    .on(
                    'touchstart',
                    function(e) {
                        if (presenter.isMinuteHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                            e.stopPropagation();
                            e.stopImmediatePropagation();
                            e.preventDefault();
                            presenter.moveCurrentHand(this);
                            presenter.isHandInMove = true;
                        }
                    })
                    .on(
                    'touchend',
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        presenter.isHandInMove = false;
                        if(!presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                        }
                    })
                    .on(
                    'touchmove',
                    function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'touch');
                            presenter.setTimeBasedOnSyncOption();

                            if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                                presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            }
                            presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }
                    });
            });

            jQuery(function($) {
                $(view)
                    .find('#h-hand')
                    .on(
                    'touchstart',
                    function(e) {
                        if (presenter.isHourHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                            e.stopPropagation();
                            e.stopImmediatePropagation();
                            e.preventDefault();
                            presenter.moveCurrentHand(this);
                            presenter.isHandInMove = true;
                        }
                    })
                    .on(
                    'touchend',
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        presenter.isHandInMove = false;
                        if(!presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                        }

                    })
                    .on(
                    'touchmove',
                    function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'touch');
                            presenter.setTimeBasedOnSyncOption();

                            if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                                presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            }
                            presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }
                    });
            });

            jQuery(function($) {
                $(view)
                    .find('#s-hand')
                    .on(
                    'touchstart',
                    function(e) {
                        if (presenter.isSecondHandActive()
                            &amp;&amp; !presenter.isDisable
                            &amp;&amp; !presenter.isErrorCheckingMode) {
                            e.stopPropagation();
                            e.stopImmediatePropagation();
                            e.preventDefault();
                            presenter.moveCurrentHand(this);
                            presenter.isHandInMove = true;
                        }
                    })
                    .on(
                    'touchend',
                    function(e) {
                        presenter.currentHand = 0;
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        presenter.isHandInMove = false;
                        if(!presenter.continousEvents) {
                            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                        }
                    })
                    .on(
                    'touchmove',
                    function(e) {
                        e.preventDefault();
                        e.stopPropagation();
                        e.stopImmediatePropagation();
                        if (presenter.isHandInMove) {
                            presenter.changeHandPosition(e, 'touch');
                            presenter.setTimeBasedOnSyncOption();

                            if (presenter.shouldSendEventTime != presenter.getCurrentTime() &amp;&amp; presenter.continousEvents) {
                                presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);
                            }
                            presenter.shouldSendEventTime = presenter.getCurrentTime();
                        }
                    });
            });

        }

    };

    presenter.setTimeBasedOnSyncOption = function() {
        if (presenter.isSynhronized === 'Min with Hour') {
            presenter.setTimeSynhronizedMinWithHour();
        } else if (presenter.isSynhronized === 'Both') {
            presenter.setTimeSynhronizedBoth();
        } else if (presenter.isSynhronized === 'All') {
            presenter.setTimeSynhronizedAll();
        } else {
            presenter.setTimeNotSynhronized();
        }
    };

    presenter.isHourHandActive = function() {
        const activeOptions = ["Hour", "H+M", "H+S", "All"];
        activeOptions.push("Both"); //for backward compatibility
        activeOptions.push("HourHand"); //for backward compatibility
        activeOptions.push(""); //for backward compatibility

        return activeOptions.includes(presenter.ActiveHand);
    };

    presenter.isMinuteHandActive = function() {
        const activeOptions = ["Minute", "H+M", "M+S", "All"];
        activeOptions.push("Both"); //for backward compatibility
        activeOptions.push("MinuteHand"); //for backward compatibility
        activeOptions.push(""); //for backward compatibility

        return activeOptions.includes(presenter.ActiveHand);
    };

    presenter.isSecondHandActive = function() {
        const activeOptions = ["Second", "H+S", "M+S", "All"];

        return activeOptions.includes(presenter.ActiveHand);
    }

    presenter.createPreview = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        if (presenter.validate(view, model)) {
            presenter.init(view, model);

            presenter.setVisibility(true);
            presenter.setClockTime(model.InitialTime);
        }

    };

    presenter.getCurrentTime = function() {
        if(presenter.currentMinuteValue == 60){
            presenter.currentMinuteValue = 0;
            if (presenter.TimeStandard == 12 &amp;&amp; presenter.currentHourValue == 12) {
                presenter.currentHourValue = 1;
            } else{
                if (presenter.TimeStandard == 24 &amp;&amp; presenter.currentHourValue == 23) {
                    presenter.currentHourValue = 0;
                } else{
                    presenter.currentHourValue++;
                }
            }
        }
        let currentTime = `${presenter.currentHourValue}:${presenter.getValueWithLeadingZero(presenter.currentMinuteValue)}`;
        if (presenter.showSecondHand) {
            currentTime = currentTime + ":" + presenter.getValueWithLeadingZero(presenter.currentSecondValue);
        }

        return currentTime;
    };

    presenter.getValueWithLeadingZero = function(value) {
        return value &lt; 10 ? "0" + value : value;
    };

    presenter.getCurrentHour = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        return presenter.currentHourValue;
    };

    presenter.getCurrentMinute = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        return presenter.currentMinuteValue &lt; 10 ? "0"
            + presenter.currentMinuteValue : presenter.currentMinuteValue;
    };

    presenter.getCurrentSeconds = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        return presenter.showSecondHand ? presenter.getValueWithLeadingZero(presenter.currentSecondValue) : null;
    };

    presenter.disable = function() {
        if (presenter.showAnswersMode &amp;&amp; !presenter.isGradualShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.isDisable = true;
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        $($myDiv).addClass('disable');
    };

    presenter.enable = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        presenter.isDisable = false;
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        $($myDiv).removeClass('disable');
    };

    /**
     * Returns current disable status of the addon.
     */
    presenter.isDisabled = function () {
        return presenter.isDisable;
    }

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.isAttempted = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        if (presenter.isActivity === false) {
            return true;
        } else {
            if (presenter.InitialTime == presenter.getCurrentTime()) {
                return false;
            } else {
                return true;
            }
        }
    };

    presenter.moveCurrentHand = function(element) {
        presenter.currentHand = element.id;
    };

    presenter.markAsCorrect = function() {
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        presenter.isErrorCheckingMode = true;
        $($myDiv).removeClass('incorrect');
        $($myDiv).addClass('correct');

    };

    presenter.markAsWrong = function() {
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        presenter.isErrorCheckingMode = true;
        $($myDiv).removeClass('correct');
        $($myDiv).addClass('incorrect');
    };

    presenter.markAsEmpty = function() {
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        presenter.isErrorCheckingMode = true;
        $($myDiv).removeClass('incorrect');
        $($myDiv).removeClass('correct');
    };

    presenter.changeHandPosition = function(e, device) {
        if (presenter.currentHand != 0 &amp;&amp; presenter.isHandInMove) {
            var h = 0, m = 0;
            var value = 0;
            var $div = presenter.$view.find('#analog-clock');

            if (device == 'mouse') {
                presenter.mouseX = e.pageX - $div.offset().left;
                presenter.mouseY = e.pageY - $div.offset().top;
            } else {
                presenter.mouseX = e.originalEvent.touches[0].pageX
                    - $div.offset().left;
                presenter.mouseY = e.originalEvent.touches[0].pageY
                    - $div.offset().top;
            }

            if (presenter.mouseX == presenter.center) {
                if (presenter.mouseY &lt; presenter.center) {
                    value = 0;
                } else {
                    if (presenter.mouseY &gt; presenter.center) {
                        value = 180;
                    }
                }

            } else {
                if (presenter.mouseY == presenter.center) {
                    if (presenter.mouseX &lt; presenter.center) {
                        value = 270;
                    } else {
                        if (presenter.mouseX &gt; presenter.center) {
                            value = 90;
                        }
                    }

                } else {
                    if (presenter.mouseX &gt; presenter.center
                        &amp;&amp; presenter.mouseY &lt; presenter.center) {
                        value = 90 - presenter.countAngle(parseInt(
                            presenter.mouseX, 10)
                            - parseInt(presenter.center, 10), parseInt(
                            presenter.center, 10)
                            - parseInt(presenter.mouseY, 10));
                    }

                    if (presenter.mouseX &lt; presenter.center
                        &amp;&amp; presenter.mouseY &lt; presenter.center) {
                        value = 270 + presenter.countAngle(parseInt(
                            presenter.center, 10)
                            - parseInt(presenter.mouseX, 10), parseInt(
                            presenter.center, 10)
                            - parseInt(presenter.mouseY, 10));
                    }

                    if (presenter.mouseX &lt; presenter.center
                        &amp;&amp; presenter.mouseY &gt; presenter.center) {
                        value = 270 - presenter.countAngle(parseInt(
                            presenter.center, 10)
                            - parseInt(presenter.mouseX, 10), parseInt(
                            presenter.mouseY, 10)
                            - parseInt(presenter.center, 10));
                    }

                    if (presenter.mouseX &gt; presenter.center
                        &amp;&amp; presenter.mouseY &gt; presenter.center) {
                        value = 90 + presenter.countAngle(parseInt(
                            presenter.mouseX, 10)
                            - parseInt(presenter.center, 10), parseInt(
                            presenter.mouseY, 10)
                            - parseInt(presenter.center, 10));

                    }

                }
            }

            if (presenter.currentHand === "m-hand") {
                value = presenter.countNewHandValue(value, presenter.Step);
            }

            if (presenter.currentHand === "s-hand") {
                value = presenter.countNewHandValue(value, presenter.StepSeconds);
            }

            presenter.setAttr(presenter.currentHand, value);
            presenter.currentHandValue = value;

        } else{
            presenter.triggerFrameChangeEvent(presenter.getCurrentTime() == presenter.CorrectAnswer ? 1	: 0);

        }

    };

    presenter.countNewHandValue = function(value, step) {
        const roundedValue = Math.floor(parseInt(value / 6) / step) * step;

        if (parseInt(value / 6) % step &gt;= parseInt(step / 2, 10)) {
            value = (roundedValue + step) * 6;
        } else {
            value = roundedValue * 6;
        }

        return value;
    };

    presenter.countAngle = function(coordinateX, coordinateY) {
        var value = parseInt(Math.atan((coordinateY / coordinateX)) * 180
            / Math.PI, 10);
        return value;
    };

    presenter.getState = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();
        }
        presenter.isErrorCheckingMode = false;
        var isVisible = presenter.isVisible;
        var wasVisible = presenter.wasVisible;
        var initialTime = presenter.InitialTime;
        var wasDisable = presenter.wasDisable;
        var isDisable = presenter.isDisable;
        var currentTime = presenter.getCurrentTime();

        return JSON.stringify({
            initialTime : initialTime,
            isVisible : isVisible,
            wasVisible : wasVisible,
            wasDisable : wasDisable,
            isDisable : isDisable,
            currentTime : currentTime
        });

    };

    presenter.setState = function(state) {
        var parsedState = JSON.parse(state), $myDiv = presenter.$view
            .find('.analog-clock')[0];
        presenter.currentTime = parsedState.currentTime;
        presenter.isVisible = parsedState.isVisible;
        presenter.wasVisible = parsedState.wasVisible;
        presenter.InitialTime = parsedState.initialTime;
        presenter.wasDisable = parsedState.wasDisable;
        presenter.isDisable = parsedState.isDisable;
        presenter.setVisibility(presenter.isVisible);

        presenter.setClockTime(presenter.currentTime);
        presenter.isDisable === true ? $($myDiv).addClass('disable')
            : $($myDiv).removeClass('disable');
    };

    presenter.reset = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }

        var $myDiv = presenter.$view.find('#analog-clock')[0];
        presenter.setWorkMode();
        presenter.isErrorCheckingMode = false;

        presenter.setClockTime(presenter.InitialTime);
        presenter.isVisible = presenter.wasVisible;
        presenter.setVisibility(presenter.wasVisible);
        presenter.isDisable = presenter.wasDisable;
        presenter.isDisable === true ? $($myDiv).addClass('disable')
            : $($myDiv).removeClass('disable');
    };

    presenter.getMaxScore = function() {
        if (presenter.CorrectAnswer === presenter.InitialTime) {
            return 0;
        }
        if (presenter.isActivity === true) {
            return 1;
        } return 0;
    };

    presenter.getScore = function() {
        var isShowAnswerActive = presenter.showAnswersMode;

        if (presenter.showAnswersMode){
            presenter.hideAnswers();
        }

        var scoreValue = presenter.getScoreValue();

        if (isShowAnswerActive) {
            presenter.showAnswers();
        }

        return scoreValue;
    };

    presenter.getScoreValue = function () {
        if (presenter.isActivity &amp;&amp; presenter.getCurrentTime() === presenter.CorrectAnswer) {
            return 1;
        } return 0;
    };

    presenter.getErrorCount = function() {
        var isShowAnswerActive = presenter.showAnswersMode;

        if(presenter.showAnswersMode){
            presenter.hideAnswers();
        }

        var errorCount = presenter.getErrorCountValue();

        if (isShowAnswerActive) {
            presenter.showAnswers();
        }

        return errorCount;
    };

    presenter.getErrorCountValue = function () {
        var isAnswerDefault = presenter.CorrectAnswer === presenter.InitialTime;
        var isAnswerCorrect = presenter.getCurrentTime() === presenter.CorrectAnswer;
        switch (true) {
            case (presenter.isActivity &amp;&amp; isAnswerDefault &amp;&amp; !isAnswerCorrect):
                return 1;

            case (presenter.isActivity &amp;&amp; presenter.neutralOption() === 1):
                return 0;

            case (presenter.isActivity):
                return presenter.getMaxScore() - presenter.getScore();

            default:
                return 0;
        }
    };

    presenter.neutralOption = function() {
        return presenter.getCurrentTime() == presenter.InitialTime ? 1 : 0;
    };

    presenter.setShowErrorsMode = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }

        presenter.isErrorCheckingMode = true;


        if (presenter.isActivity === true) {
            var $myDiv = presenter.$view.find('.analog-clock')[0];

            if (presenter.neutralOption() === 0) {

                if (presenter.getScore() === presenter.getMaxScore()
                    &amp;&amp; presenter.getErrorCount() === 0) {
                    $($myDiv).addClass('correct');
                } else {
                    $($myDiv).addClass('incorrect');
                }
            }

        }
    };

    presenter.setWorkMode = function() {
        if(presenter.showAnswersMode === true){
            presenter.hideAnswers();

        }
        var $myDiv = presenter.$view.find('.analog-clock')[0];
        presenter.isErrorCheckingMode = false;
        $($myDiv).removeClass('correct');
        $($myDiv).removeClass('incorrect');
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(checkScore) {
        return {
            source : presenter.modelID,
            item : "" + "1",
            value : presenter.getCurrentTime(),
            score : '' + checkScore
        };
    };

    presenter.triggerFrameChangeEvent = function(checkScore) {
        var eventData = presenter.createEventData(checkScore);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.showAnswers = function () {
        if(presenter.isErrorCheckingMode){
            presenter.setWorkMode();
        }
        if (presenter.isActivity === true) {
            presenter.isErrorCheckingMode = true;
            presenter.showAnswersMode = true;
            var $myDiv = presenter.$view.find('.analog-clock')[0];
            $($myDiv).addClass('showAnswers');
            presenter.showAnswersCurrentTime = presenter.getCurrentTime();
            presenter.setShowAnswerTime(presenter.CorrectAnswer);
        }
    };

    presenter.hideAnswers = function () {
        if(presenter.showAnswersMode === true){
            var $myDiv = presenter.$view.find('.analog-clock')[0];
            $($myDiv).removeClass('showAnswers');
            presenter.isErrorCheckingMode = false;
            presenter.showAnswersMode = false;
            presenter.setShowAnswerTime(presenter.showAnswersCurrentTime);
        }
    };

    presenter.gradualShowAnswers = function (data) {
        if (data.moduleID !== presenter.modelID) {
            return;
        }
        presenter.isGradualShowAnswersActive = true;
        presenter.showAnswers();
    };

    presenter.getActivitiesCount = function () {
        return 1;
    }

    presenter.setShowAnswerTime = function(time) {
        if (presenter.showSecondHand) {
            presenter.setClockTimeWithSeconds(time);
            return;
        }

        var newTime = presenter.getNewTime(time);

        presenter.currentMinuteValue = parseInt(newTime[1], 10);
        presenter.currentHourValue = parseInt(newTime[0], 10)
            % presenter.TimeStandard;
        if (presenter.TimeStandard == 12 &amp;&amp; presenter.currentHourValue == 0) {
            presenter.currentHourValue = 12;
        }
        var h = 30 * ((parseInt(presenter.currentHourValue, 10) % 12) + parseInt(
            presenter.currentMinuteValue, 10) / 60);
        var m = 6 * parseInt(presenter.currentMinuteValue, 10);

        presenter.currentHourAngle = h;

        presenter.setAttr('h-hand', h);
        presenter.setAttr('m-hand', m);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case 'GradualShowAnswers':
                presenter.gradualShowAnswers(eventData);
                break;

            case 'ShowAnswers':
                presenter.showAnswers();
                break;

            case 'GradualHideAnswers':
                presenter.isGradualShowAnswersActive = false;
            case 'HideAnswers':
                presenter.hideAnswers();
                break;
        }
    };
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Coloring" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Image" name="Image" nameLabel="Coloring_property_image" type="image"/>
        <property displayName="Areas" name="Areas" nameLabel="Coloring_property_areas" type="text"/>
        <property displayName="Default Filling Color" name="DefaultFillingColor" nameLabel="Coloring_property_default_filling_color" type="string"/>
        <property displayName="Colors" name="colors" nameLabel="Coloring_property_colors" type="list">
            <property displayName="Description" isLocalized="true" name="speechText" nameLabel="Coloring_property_description" type="string"/>
            <property displayName="Color RGBA" name="colorRGBA" nameLabel="Coloring_property_color" type="string"/>
        </property>
        <property displayName="Tolerance" name="Tolerance" nameLabel="Coloring_property_tolerance" type="string"/>
        <property displayName="Is Not Activity" name="isNotActivity" nameLabel="Coloring_property_is_not_activity" type="boolean"/>
        <property displayName="Is Disabled" name="isDisabled" nameLabel="Coloring_property_is_disabled" type="boolean"/>
        <property displayName="Disable fill on click" name="disableFill" nameLabel="Coloring_property_disable_fill" type="boolean"/>
        <property displayName="Color correct" name="colorCorrect" nameLabel="Coloring_property_color_correct" type="boolean"/>
        <property displayName="Mark transparent areas" name="markTransparentAreas" nameLabel="Coloring_property_mark_transparent_areas" type="boolean"/>
        <property displayName="Show all answers in gradual show answers mode" name="showAllAnswersInGradualShowAnswersMode" nameLabel="Coloring_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Coloring_property_speech_texts" type="staticlist">
            <property name="Area" nameLabel="Coloring_speech_text_area" type="staticrow">
                    <property name="Area" nameLabel="Coloring_speech_text_area" type="string"/>
            </property>
            <property name="Color" nameLabel="Coloring_speech_text_color" type="staticrow">
                    <property name="Color" nameLabel="Coloring_speech_text_color" type="string"/>
            </property>
            <property name="Selected" nameLabel="Coloring_speech_text_selected" type="staticrow">
                    <property name="Selected" nameLabel="Coloring_speech_text_selected" type="string"/>
            </property>
            <property name="Correct" nameLabel="Coloring_speech_text_correct" type="staticrow">
                    <property name="Correct" nameLabel="Coloring_speech_text_correct" type="string"/>
            </property>
            <property name="Incorrect" nameLabel="Coloring_speech_text_incorrect" type="staticrow">
                    <property name="Incorrect" nameLabel="Coloring_speech_text_incorrect" type="string"/>
            </property>
        </property>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Coloring_property_lang_attribute" type="string"/>
    </model>
<css>.coloring-wrapper .coloring-container canvas {
    display: table;
    margin: auto;
}

.coloring-wrapper {
    display: table;
    width: 100%;
    height: 100%;
    overflow: hidden;
    outline: none;
}

.coloring-circle-mark {
    background-color: transparent;
    width: 20px;
    height: 20px;
    border-radius: 100%;
    border: 1mm solid lightblue;
    position : absolute;
}

.coloring-color-list {
    display: flex;
    justify-content: center;
    gap: 2px;
    background-color: white;
    box-shadow: 0px 0.2px 0px 0.2px grey;
    align-items: center;
    align-content: center;
    flex-direction: column;
    position : absolute;
}

.coloring-color-list-element {
    display: flex;
    min-width: 80px;
    padding: 5px;
    justify-content: center;
    align-items: center;
    align-content: center;
    background-color: white;
    box-shadow: 0px 0.2px 0px 0.2px grey;
}

.coloring-wrapper .coloring-container {
    display: table-cell;
    vertical-align: middle;
}

.coloring-wrapper .coloring-container .icon-container {
    border: 1px solid #111111;
    border-radius: 50%;
    font-size: 10px;
    height: 8px;
    line-height: 6px;
    padding: 2px;
    position: absolute;
    width: 8px;
    font-weight: bold;
}

.coloring-wrapper .coloring-container .icon-container.wrong {
    background-color: #ff5599;
}

.coloring-wrapper .coloring-container .icon-container.correct {
    background-color: #55ff99;
}

.coloring-wrapper .coloring-container .icon-container.correct:after {
    content: 'v';
}

.coloring-wrapper .coloring-container .icon-container.wrong:after {
    content: 'x';
}
</css><view>&lt;div class="coloring-wrapper"&gt;
    &lt;div class="coloring-container"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="coloring-wrapper"&gt;
    &lt;div class="coloring-container"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonColoring_create(){
    /*
        KNOWN ISSUES:
            Areas - property validation - backward compatibility:
                Co-ordinates X &amp; Y:
                    Due to invalid validation, those values can be negative numbers or non-numbers strings. The addon behavior is to
                    display image, pass the validation and just do nothing. Browser probably will throw a small stack trace with function's
                    referring to getImageData on canvas. You shouldn't broke this invalid validation due to backward compatibility
                Transparent color:
                    It's have been added with validation : only numbers between range 0-255, without inproper strings

            Default Filling Color - backward compatibility:
                Empty string:
                    Default color is [255, 100, 100, 255], when provided string is just empty one.

    * */

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.lastEvent = null;
    presenter.imageHasBeenLoaded = false;
    presenter.shouldResetAfterCanvasInit = false;
    presenter.keyboardControllerObject = null;
    presenter.colorSpeechTextMap = {};
    presenter.isTTSExitBlocked = false;
    presenter.initialState = null;
    presenter.isCanvasInitiated = false;
    presenter.defaultColorRGBA = [255, 100, 100, 255];
    presenter.whiteRGBA = [255, 255, 255, 255];
    presenter.transparentAreaTTS = "transparent";

    presenter.AREA_TYPE = {
        NORMAL: 0,
        TRANSPARENT: 1,
        USER_AREA: 2
    };

    presenter.DEFAULT_TTS_PHRASES = {
        area: "Area",
        color: "Color",
        selected: "Selected",
        correct: "Correct",
        incorrect: "Incorrect",
    };

    presenter.errorCodes = {
        'E01': 'Wrong color notation. Must be in "r g b a" format. See documentation for more details.',
        'E02': 'All color values must be between 0 - 255.',
        'E03': 'Areas are configured wrong. It should be in "x; y; color; optional TTS description" format. See documentation for more details.',
        'E04': 'Areas x &amp; y values have to be smaller than Width and Height properties.',
        'E05': 'Tolerance value must be between 0 - 100',
        'E06': 'Areas are configured wrong. Color has an invalid value, or TTS description is not properly separated. See documentation for more details',
        'E07': 'Colors property is configured wrong. "Description" field must not be empty.',
        'A01': "Areas x &amp; y values have to be integer values between 0 - 255."
    };

    function getErrorObject (errorCode) { return { isValid: false, isError: true, errorCode }; }
    function getCorrectObject (value) { return { isValid: true, isError: false, value }; }

    function areaObject(x, y, type) {
        this.x = x;
        this.y = y;
        this.type = type;
        this.defaultColor;
        this.pixelPosition;
        this.colorToFill = [];

        this.getColor = function () {
            return presenter.getColorAtPoint(this.x, this.y);
        };

        this.setPixelPosition = function () {
            this.pixelPosition = ((this.x + this.y * presenter.canvasWidth) * 4);
        };
    }

    presenter.createPreview = function(view, model){
        runLogic(view, model, true);
    };

    function setColorsThatCanBeFilled() {
        var configuration = presenter.configuration;
        configuration.colorsThatCanBeFilled = [];
        const wholeCanvas = presenter.ctx.getImageData(0, 0, presenter.canvasWidth, presenter.canvasHeight).data;
        const areas = configuration.areas;

        for (var i = areas.length; i--;) {
            let area = areas[i];
            let color = getColorAtPointFromCanvas(area.x, area.y, wholeCanvas);
            if (!presenter.isAlreadyInColorsThatCanBeFilled(color)) {
                configuration.colorsThatCanBeFilled.push(color);
            }
        }
    }

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;

        presenter.runEndedDeferred = new $.Deferred();
        presenter.runEnded = presenter.runEndedDeferred.promise();
    };

    presenter.createEventData = function (item, value, score) {
        var score;

        if (ModelValidationUtils.isStringEmpty(score)) {
            score = "";
        } else {
            score = score ? 1 : 0;
        }

        return {
            'source': presenter.configuration.addonID,
            'item': item.join(';'),
            'value': value.toString(),
            'score': score
        };
    };

    presenter.sendEvent = function(item, value, score) {
        if (!presenter.isShowAnswersActive &amp;&amp; !presenter.isShowErrorsModeActive) {
            var eventData = presenter.createEventData(item, value, score);
            presenter.eventBus.sendEvent('ValueChanged', eventData);

            if (presenter.isAllOK()) sendAllOKEvent();
        }
    };

    function sendAllOKEvent() {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    }

    function setAreasDefaultColors() {
        var configuration = presenter.configuration;
        $.each(configuration.areas, function() {
            this.defaultColor = presenter.getColorAtPoint(this.x, this.y);
        });
    }

    function setAreasPixelPosition() {
        var configuration = presenter.configuration;
        $.each(configuration.areas, function() {
            this.pixelPosition = (this.x + this.y * presenter.canvasWidth) * 4;
        });
    }

    function getClickedArea(clickObject) {
        var configuration = presenter.configuration;
        var clickedArea = new areaObject(clickObject.x, clickObject.y, presenter.AREA_TYPE.USER_AREA);
        clickedArea.setPixelPosition();
        clickedArea.colorToFill = presenter.whiteRGBA;

        for(var i = 0; i &lt; configuration.areas.length; i++) {
            var area = configuration.areas[i];

            if(isAreaColored(area)) {
                presenter.allColoredPixels = [];
                return area;
            }
        }

        if (presenter.configuration.userAreas == undefined) {
            presenter.configuration.userAreas = [];
        }

        if (!isUserAreaExists()) {
            presenter.configuration.userAreas.push(clickedArea);
        }

        presenter.allColoredPixels = [];
        return clickedArea;
    }

    function isUserAreaExists() {
        var userAreas = presenter.configuration.userAreas;
        for(var i = 0; i &lt; userAreas.length; i++) {
            if(isAreaColored(userAreas[i])) {
                return true;
            }
        }

        return false;
    }

    function isAreaColored(area) {
        return (presenter.allColoredPixels.indexOf(area.pixelPosition) != -1);
    }

    presenter.clearArea = function (x, y, isRemovingWrongColor) {
        presenter.fillArea(x, y, '255 255 255 255', isRemovingWrongColor);
    };

    presenter.fillArea = function (x, y, color, isRemovingWrongColor) {
        presenter.isColored = true;

        presenter.click = {
            x: parseInt(x, 10),
            y: parseInt(y, 10)
        };

        presenter.click.color = presenter.getColorAtPoint(presenter.click.x, presenter.click.y);

        if (color == undefined) {
            presenter.fillColor = presenter.configuration.currentFillingColor;
        } else {
            var validatedDefaultFillingColor = presenter.validateColor(color);
            if (validatedDefaultFillingColor.isError) {
                DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.errorCodes, validatedDefaultFillingColor.errorCode);
                return;
            }
            presenter.fillColor = validatedDefaultFillingColor.value;
        }

        if ( presenter.isAlreadyInColorsThatCanBeFilled(presenter.click.color) || isRemovingWrongColor) {
            if (!presenter.isShowAnswersActive &amp;&amp; !presenter.isShowErrorsModeActive) {
                presenter.floodFill(
                    presenter.click,
                    presenter.fillColor,
                    presenter.configuration.tolerance
                );
            }

            if (!presenter.isAlreadyInColorsThatCanBeFilled(presenter.fillColor)) {
                presenter.configuration.colorsThatCanBeFilled.push(presenter.fillColor)
            }
        }

        presenter.allColoredPixels = [];
    };

    presenter.checkIfColoredCorrectly = function () {
        $.each(presenter.configuration.areas, function() {
            var area = this;

            if(!presenter.shouldBeTakenIntoConsideration(area)) {
                return true; // continue
            }

            if (!isCorrect(area)) {
                    var r = area.colorToFill[0],
                        g = area.colorToFill[1],
                        b = area.colorToFill[2],
                        a = area.colorToFill[3],
                        color = r + " " + g + " " + b + " " + a;
                if(r != -1){
                    presenter.fillArea(area.x, area.y, color, false);
                }else{
                    presenter.fillArea(area.x, area.y, '0 0 0 0', false);
                }
            }
        });
    };

    presenter.clickLogic = function(e, isTouch) {
        e.stopPropagation();
        e.preventDefault();

        presenter.isColored = true;

        presenter.click = getMousePositionOnCanvas(e, isTouch);

        presenter.click.color = presenter.getColorAtPoint(presenter.click.x, presenter.click.y);

        if (presenter.configuration.disableFill) {
            presenter.sendEvent([presenter.click.x, presenter.click.y], '', '');

            return false;
        }

        if ( presenter.isAlreadyInColorsThatCanBeFilled(presenter.click.color) ) {

            if (!presenter.isShowAnswersActive &amp;&amp; !presenter.isShowErrorsModeActive) {
                presenter.floodFill(
                    presenter.click,
                    presenter.configuration.currentFillingColor,
                    presenter.configuration.tolerance
                );
            }

            if(!presenter.configuration.colorCorrect){
                setTimeout(function(){
                    // Without timeout there are issues on Firefox if event handling takes too long
                    presenter.userInteractionSendingEvent(getClickedArea(presenter.click));
                }, 0);
            }

            if (!presenter.isAlreadyInColorsThatCanBeFilled(presenter.configuration.currentFillingColor)) {
                presenter.configuration.colorsThatCanBeFilled.push(presenter.configuration.currentFillingColor)
            }
        }
        if(presenter.configuration.colorCorrect){
            presenter.checkIfColoredCorrectly();
            setTimeout(function(){
                // Without timeout there are issues on Firefox if event handling takes too long
                presenter.sendEvent([presenter.click.x, presenter.click.y], presenter.configuration.isErase ? 0 : 1, 1);
            }, 0);
        }
    };

    presenter.userInteractionSendingEvent = function (clickedArea) {
        if (clickedArea.type == presenter.AREA_TYPE.USER_AREA) {
            presenter.sendEvent([clickedArea.x, clickedArea.y], presenter.configuration.isErase ? 0 : 1, "");
        } else {
            presenter.sendEvent([clickedArea.x, clickedArea.y], presenter.configuration.isErase ? 0 : 1, isCorrect(clickedArea) ? 1 : 0);
        }
    };

    presenter.recolorImage = function () {
        var imageData = presenter.ctx.getImageData(0, 0,presenter.canvasWidth, presenter.canvasHeight);

        for (var i=0; i&lt;imageData.data.length; i+=4) {
            if (imageData.data[i]==0 &amp;&amp;
                imageData.data[i+1]==0 &amp;&amp;
                imageData.data[i+2]==0 &amp;&amp;
                imageData.data[i+3]==255
                ){
                imageData.data[i]=55;
                imageData.data[i+1]=55;
                imageData.data[i+2]=55;
                imageData.data[i+3]=255;
            }
        }

        presenter.ctx.putImageData(imageData,0,0);
    };

    presenter.upgradeModel = function(model) {
        let upgradedModel = presenter.upgradeShowAllAnswersInGradualShowAnswersMode(model);
        upgradedModel = presenter.upgradeColors(upgradedModel);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeMarkTransparentAreas(upgradedModel);

        return upgradedModel;
    };

    presenter.upgradeShowAllAnswersInGradualShowAnswersMode = function AddonColoring_upgradeShowAllAnswersInGradualShowAnswersMode (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['showAllAnswersInGradualShowAnswersMode']) {
            upgradedModel['showAllAnswersInGradualShowAnswersMode'] = false;
        }

        return upgradedModel;
    }

    presenter.upgradeColors = function AddonColoring_upgradeColors (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);
        if (!upgradedModel['colors']) {
            upgradedModel['colors'] = [];
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function AddonColoring_upgradeSpeechTexts (model) {
        const defaultValue = {
            Area: { Area: "" },
            Color: { Color: "" },
            Selected: { Selected: "" },
            Correct: { Correct: "" },
            Incorrect: { Incorrect: "" }
        }

        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object
        if (model["speechTexts"] === undefined) {
            upgradedModel["speechTexts"] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function AddonColoring_upgradeLangTag (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);
        if (model["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeMarkTransparentAreas = function AddonColoring_upgradeMarkTransparentAreas (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);
        if (!model.hasOwnProperty('markTransparentAreas')) {
            upgradedModel['markTransparentAreas'] = "False";
        }

        return upgradedModel;
    };


    presenter.validateModel = function(model, isPreview) {
        const validatedAreas = model['Areas'].toString().length
            ? presenter.validateAreas(model['Areas'], isPreview, model['Width'], model['Height'])
            : getCorrectObject([]);
        if (validatedAreas.isError) return validatedAreas;

        const validatedTolerance = model['Tolerance'].toString().length
            ? ModelValidationUtils.validateIntegerInRange(model['Tolerance'], 100, 0)
            : getCorrectObject(50);
         if (!validatedTolerance.isValid) return getErrorObject('E05');

        const validatedDefaultFillingColor = model['DefaultFillingColor'].toString().length
            ? presenter.validateColor(model['DefaultFillingColor'])
            : getCorrectObject(presenter.defaultColorRGBA);
        if (validatedDefaultFillingColor.isError) return validatedDefaultFillingColor;

        const validatedColors = presenter.validateColors(model['colors']);
        const colorsError = validatedColors.find(colorObject =&gt; colorObject.isError);
        if (colorsError) return colorsError;

        const validatedIsVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        const validatedIsDisabled = ModelValidationUtils.validateBoolean(model['isDisabled']);

        return {
            'isValid': true,
            'isError': false,
            'addonID' : model['ID'],
            'imageFile': model.Image,
            'areas' : validatedAreas.value,
            'tolerance' : validatedTolerance.value,
            'currentFillingColor' : validatedDefaultFillingColor.value,
            'defaultFillingColor' : validatedDefaultFillingColor.value,
            'colors' : validatedColors,
            'isErase' : false,
            'isVisible' : validatedIsVisible,
            'isVisibleByDefault' : validatedIsVisible,
            'isDisabled' : validatedIsDisabled,
            'isDisabledByDefault' : validatedIsDisabled,
            'isActivity' : !(ModelValidationUtils.validateBoolean(model['isNotActivity'])),
            'lastUsedColor' : validatedDefaultFillingColor.value,
            'disableFill' : ModelValidationUtils.validateBoolean(model['disableFill']),
            'colorCorrect' : ModelValidationUtils.validateBoolean(model.colorCorrect),
            'showAllAnswersInGradualShowAnswersMode' : ModelValidationUtils.validateBoolean(model.showAllAnswersInGradualShowAnswersMode),
            'langTag': model.langAttribute,
            'markTransparentAreas': ModelValidationUtils.validateBoolean(model.markTransparentAreas)
        }
    };

    presenter.validateAreas = function(areasText, isPreview, modelWidth, modelHeight) {
        modelWidth = parseInt(modelWidth, 10);
        modelHeight = parseInt(modelHeight, 10);
        const areas = Helpers.splitLines(areasText)
            .map(element =&gt; element.split(';'))
            .map(element =&gt; presenter.validateArea(element, isPreview, modelWidth, modelHeight));

        const errors = areas.filter(element =&gt; element.isError);
        if (errors.length) {
            return errors[0];
        }

        return getCorrectObject(areas);
    };

    presenter.validateArea = function(element, isPreview, modelWidth, modelHeight) {
        if (element.length === 3 || element.length === 4) {
            const trimmedArray = element.map(value =&gt; value.trim());
            if (presenter.isTransparent(trimmedArray)) {
                return presenter.parseTransparentArea(trimmedArray);
            }
            const area = {
                x: parseInt(trimmedArray[0], 10),
                y: parseInt(trimmedArray[1], 10),
                type: presenter.AREA_TYPE.NORMAL,
                speechText: trimmedArray[3] ? trimmedArray[3] : presenter.speechTexts.Area,
            };

            if (isPreview &amp;&amp; (area.x &gt;= modelWidth || area.y &gt;= modelHeight)) {
                return getErrorObject('E04');
            }

            const validatedColor = presenter.validateColor(trimmedArray[2]);
            if (validatedColor.isError) return getErrorObject("E06");

            area.colorToFill = validatedColor.value;
            area.isError = false;
            return area;
        }
        return getErrorObject('E03');
    }


    presenter.isTransparent = function (value) {
        return value[2] === "transparent";
    };

    presenter.parseTransparentArea = function (splittedAreaArray) {
        const area = {
            x: parseInt(Number(splittedAreaArray[0])),
            y: parseInt(Number(splittedAreaArray[1])),
            type: presenter.AREA_TYPE.TRANSPARENT,
            colorToFill: [-1, -1, -1, -1],
            speechText: splittedAreaArray[3] ? splittedAreaArray[3] : presenter.speechTexts.Area
        };

        if (isNaN(area.x) || isNaN(area.y)) {
            return getErrorObject("A01");
        }
        if (area.x &lt; 0 || area.y &lt; 0) {
            return getErrorObject("A01");
        }

        area.isError = false;

        return area;
    };

    presenter.validateColor = function(spaceSeparatedColor) {
        const splitted = spaceSeparatedColor.split(' '),
            validatedColors = [];

        if (splitted.length !== 4) {
            return getErrorObject('E01');
        }

        let areAllValuesInRange = true;
        $.each(splitted, function() {
            const validated = ModelValidationUtils.validateIntegerInRange(this, 255, 0);
            if (!validated.isValid) {
                areAllValuesInRange = false;
                return;
            }
            validatedColors.push(validated.value);
        });

        if (!areAllValuesInRange) {
            return getErrorObject('E02');
        }

        return getCorrectObject(validatedColors);
    };

    presenter.validateColors = function (colorsModel) {
        return colorsModel
            .filter(colorObject =&gt; colorObject?.colorRGBA.length || colorObject?.speechText.length)
            .map(colorObject =&gt; {
                const colorValidation = presenter.validateColor(colorObject.colorRGBA)
                const speechText = colorObject.speechText?.length
                    ? colorObject.speechText
                    : getErrorObject("E07");

                if (colorValidation.isError) return colorValidation;
                if (speechText.isError) return speechText;

                return {
                        speechText,
                        colorRGBA: colorValidation.value
                    }
            });
    }

    function runLogic(view, model, isPreview) {
        model = presenter.upgradeModel(model);
        presenter.setSpeechTexts(model["speechTexts"]);

        presenter.configuration = presenter.validateModel(model, isPreview);
        presenter.createColorSpeechTextsMap(presenter.configuration.colors);
        presenter.allColoredPixels = [];
        presenter.currentAreaIdInGSAMode = 0;

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage($(view), presenter.errorCodes, presenter.configuration.errorCode);
            return;
        }

        presenter.$view = $(view);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        presenter.setImageElement(isPreview);
        presenter.buildKeyboardController();
    }

    presenter.createColorSpeechTextsMap = function(colors) {
        if (!colors) return;
        const whiteRGBAString = presenter.getRGBAStringFromRGBAArray(presenter.whiteRGBA);
        presenter.colorSpeechTextMap = colors.reduce((prev, curr) =&gt; {
            const color = presenter.getRGBAStringFromRGBAArray(curr.colorRGBA);
            if (color === whiteRGBAString) return prev;
            return {
                ...prev,
                [color] : curr.speechText
            }
        }, {});
    }

    presenter.setImageElement = function (isPreview) {
        const $imageElement = $('&lt;img&gt;');

        URLUtils.prepareImageForCanvas(presenter.playerController, $imageElement[0], presenter.configuration.imageFile);

        const canvasElement = $('&lt;canvas&gt;&lt;/canvas&gt;');
        presenter.ctx = canvasElement[0].getContext('2d');

        $imageElement.load(function() {
            canvasElement.attr('width', $imageElement[0].width);
            canvasElement.attr('height', $imageElement[0].height);
            presenter.canvasWidth = $imageElement[0].width;
            presenter.canvasHeight = $imageElement[0].height;
            presenter.canvas = canvasElement[0];

            presenter.ctx.drawImage($imageElement[0], 0, 0);
            presenter.imageHasBeenLoaded = true;
            presenter.imageData = presenter.ctx.getImageData(0, 0, $imageElement[0].width, $imageElement[0].height);
            presenter.image = $imageElement;

            const coloringContainer = presenter.$view.find('.coloring-container');
            coloringContainer.append(canvasElement);

            presenter.canvasOffset = canvasElement.offset();

            setColorsThatCanBeFilled();
            setAreasDefaultColors();
            setAreasPixelPosition();

            if (isPreview) {
                const coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
                    xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                    yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                    coloringWrapper = presenter.$view.find('.coloring-wrapper');

                coordinatesContainer.css({
                    'width' : 60,
                    'height' : 25,
                    'border' : '1px solid #696969',
                    'borderRadius' : '3px',
                    'position' : 'absolute',
                    'top' : 5,
                    'left' : 5,
                    'fontSize' : '9px',
                    'padding': '5px',
                    'lineHeight' : '12px'
                });

                coordinatesContainer
                    .append(xContainer)
                    .append(yContainer);

                coloringWrapper.append(coordinatesContainer);
                coloringWrapper.css({
                    'position' : 'relative',
                    'minHeight' : presenter.canvasHeight,
                    'minWidth' : presenter.canvasWidth
                });

                const moduleSelector = $('.moduleSelector[data-id="'+presenter.configuration.addonID+'"]');

                moduleSelector.on('mousemove', function(e) {
                    xContainer.find('.value').html(getMousePositionOnCanvas(e).x);
                    yContainer.find('.value').html(getMousePositionOnCanvas(e).y);
                });

                canvasElement.on('mousemove', function(e) {
                    xContainer.find('.value').html(getMousePositionOnCanvas(e).x);
                    yContainer.find('.value').html(getMousePositionOnCanvas(e).y);
                });
            } else if (!presenter.configuration.isDisabled) {
                canvasElement.on('click', function(e) {
                    presenter.clickLogic(e);
                });

                canvasElement.on('touchstart', function (e){
                    presenter.lastEvent = e;
                });

                canvasElement.on('touchend', function (e){
                    if (presenter.lastEvent.type != e.type) {
                        presenter.clickLogic(e, true);
                    }
                });

                presenter.recolorImage();
                presenter.runEndedDeferred.resolve();
                presenter.isCanvasInitiated = true;
            }
        });
    }

    presenter.setSpeechTexts = function AddonColoring_setSpeechTexts(speechTexts) {
        presenter.speechTexts = {
            ...presenter.DEFAULT_TTS_PHRASES
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            area: TTSUtils.getSpeechTextProperty(
                speechTexts.Area.Area,
                presenter.speechTexts.area),
            color: TTSUtils.getSpeechTextProperty(
                speechTexts.Color.Color,
                presenter.speechTexts.color),
            selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.selected),
            correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.correct),
            incorrect: TTSUtils.getSpeechTextProperty(
                speechTexts.Incorrect.Incorrect,
                presenter.speechTexts.incorrect)
        };
    };

    presenter.setEventBus = function (wrappedEventBus) {
        presenter.eventBus = wrappedEventBus;

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('GradualShowAnswers', this);
        presenter.eventBus.addEventListener('GradualHideAnswers', this);
    };

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    presenter.isAlreadyInColorsThatCanBeFilled = function(color) {
        for (let i = 0; i &lt; presenter.configuration.colorsThatCanBeFilled.length; i++) {
            if (presenter.compareArrays(color, presenter.configuration.colorsThatCanBeFilled[i])) {
                return true;
            }
        }
        return false;
    };

    presenter.compareArrays = function(array1, array2) {
        // if the other array is a falsy value, return
        if (!array2)
            return false;

        // compare lengths - can save a lot of time
        if (array1.length != array2.length)
            return false;

        for (var i = 0, l=array1.length; i &lt; l; i++) {
            if (array1[i] != array2[i] &amp;&amp; (array1[i]+1) != array2[i] &amp;&amp; (array1[i]-1) != array2[i]) {
                //Due to the lossy nature of converting to and from premultiplied alpha color values,
                // pixels that have just been set using putImageData() might be returned to an equivalent getImageData() as different values. http://www.w3.org/TR/2dcontext/#dom-context-2d-getimagedata
                // Warning - two different object instances will never be equal: {x:20} != {x:20}
                return false;
            }
        }
        return true;
    };

    presenter.getColorAtPoint = function(x, y) {
        var data = presenter.ctx.getImageData(x, y, 1, 1).data,
            color = [];
        for (var i = 0; i &lt; data.length; i++) {
            color.push(data[i]);
        }
        return color;
    };

    function getColorAtPointFromCanvas (x, y, canvas) {
        // canvas is flat - 1 dimensional
        // data is flatened by going through whole X axis before incrementing Y axis - e.g. (x:1,y:0),(x:2,y:0) etc.
        // so to jump to desired position of pixel (Y:area.y, X:area.x) we need to multiply by width - that gives us desired row
        // and then add X position (area.x) - that gives us desired column
        // multiply by 4 is because wholeCanvas doesnt store px but R,G,B,A values, so each px takes 4 places in Canvas array
        let index = (y * presenter.canvasWidth + x ) * 4;
        let color = [
          canvas[index],     //R
          canvas[index + 1], //G
          canvas[index + 2], //B
          canvas[index + 3]  //A
        ];
        return color;
    }

    function fixTouch (touch) {
        var winPageX = window.pageXOffset,
            winPageY = window.pageYOffset,
            x = touch.clientX,
            y = touch.clientY;

        if (touch.pageY === 0 &amp;&amp; Math.floor(y) &gt; Math.floor(touch.pageY) ||
            touch.pageX === 0 &amp;&amp; Math.floor(x) &gt; Math.floor(touch.pageX)) {
            // iOS4 clientX/clientY have the value that should have been
            // in pageX/pageY. While pageX/page/ have the value 0
            x = x - winPageX;
            y = y - winPageY;
        } else if (y &lt; (touch.pageY - winPageY) || x &lt; (touch.pageX - winPageX) ) {
            // Some Android browsers have totally bogus values for clientX/Y
            // when scrolling/zooming a page. Detectable since clientX/clientY
            // should never be smaller than pageX/pageY minus page scroll
            x = touch.pageX - winPageX;
            y = touch.pageY - winPageY;
        }

        return {
            x: x,
            y: y
        };
    }

    function getMousePositionOnCanvas(e, isTouch) {
        var rect = presenter.canvas.getBoundingClientRect(),
            client = {
                x: e.clientX,
                y: e.clientY
            };

        if (isTouch) {
            client = fixTouch(event.touches[0] || event.changedTouches[0]);
        }

        var positionX = parseInt(client.x - rect.left, 10);
        var positionY = parseInt(client.y - rect.top, 10);
        var scaledPoint = scalePoint({x: positionX, y: positionY});

        return {x: scaledPoint.x, y: scaledPoint.y};
    }

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.disable = function() {
        presenter.configuration.isDisabled = true;
        $(presenter.canvas).off('click touchstart touchend');
        $(presenter.canvas).on('click touchstart touchend', function(e) {
            e.stopPropagation();
        });
    };

    presenter.enable = function() {
        presenter.configuration.isDisabled = false;
        $(presenter.canvas).off('click');
        $(presenter.canvas).on('click', function(e){
            presenter.clickLogic(e);
        });

        $(presenter.canvas).off('touchstart');
        $(presenter.canvas).on('touchstart', function (e){
            e.stopPropagation();
            e.preventDefault();

            presenter.lastEvent = e;
        });

        $(presenter.canvas).off('touchend');
        $(presenter.canvas).on('touchend', function (e){
            e.stopPropagation();
            e.preventDefault();

            if ( presenter.lastEvent.type != e.type ) {
                presenter.clickLogic(e, true);
            }
        });
    };

    presenter.getView = function() {
        return presenter.$view;
    };

    presenter.isAllOK = function() {
        var actualScore = presenter.getScore();

        if (presenter.configuration.transparentAreaError) {
            return false;
        }

        return actualScore === presenter.getMaxScore();
    };

    presenter.isAttempted = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        return presenter.isColored || false;
    };

    presenter.getColor = function(x, y) {
        if (x === undefined || y === undefined) return;

        return presenter.getColorAtPoint(x, y).join(' ');
    };

    presenter.removeWrongColors = function () {
        $.each(presenter.configuration.areas, function() {
            var area = this;

            if(!presenter.shouldBeTakenIntoConsideration(area)) {
                return true; // continue
            }

            if (!isCorrect(area)) {
                presenter.clearArea(area.x, area.y, true);
            }
        });
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'disable' : presenter.disable,
            'enable' : presenter.enable,
            'isAllOK' : presenter.isAllOK,
            'getView' : presenter.getView,
            'setColor' : presenter.setColorCommand,
            'setEraserOn' : presenter.setEraserOn,
            'isAttempted' : presenter.isAttempted,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'fillArea' : presenter.fillAreaCommand,
            'clearArea' : presenter.clearAreaCommand,
            'getColor' : presenter.getColor,
            'removeWrongColors': presenter.removeWrongColors
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setEraserOn = function() {
        presenter.configuration.isErase = true;
        presenter.configuration.lastUsedColor = presenter.configuration.currentFillingColor;
        presenter.configuration.currentFillingColor = presenter.whiteRGBA;
    };

    presenter.clearAreaCommand = function (coordinates){
        presenter.clearArea(coordinates[0], coordinates[1], false);
    };

    presenter.fillAreaCommand = function(coordinatesAndColor) {
        presenter.fillArea(coordinatesAndColor[0], coordinatesAndColor[1], coordinatesAndColor[2], false);
    };

    presenter.setColorCommand = function(color) {
        presenter.setColor(color[0]);
    };

    presenter.setColor = function(color) {
        var validatedDefaultFillingColor = presenter.validateColor(color);
        if (validatedDefaultFillingColor.isError) {
            DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.errorCodes, validatedDefaultFillingColor.errorCode);
            return;
        }

        presenter.configuration.currentFillingColor = validatedDefaultFillingColor.value;
        presenter.configuration.isErase = false;
    };

    presenter.activateErrorsMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (presenter.configuration.isActivity) {
            $.each(presenter.configuration.areas, function() {
                var area = this;

                if(!presenter.shouldBeTakenIntoConsideration(area)) {
                    return true; // continue
                }

                if (isCorrect(area)) {
                    displayIcon(area, false);
                } else {
                    displayIcon(area, true);
                }
            });
        }

        presenter.isShowErrorsModeActive = true;
    }

    function waitForElement(callback, timeout = 5000) {
        const startTime = Date.now();

        let interval = setInterval(() =&gt; {
            if (!$.isEmptyObject(presenter.canvas)) {
                clearInterval(interval);
                callback();
            } else if (Date.now() - startTime &gt; timeout) {
                clearInterval(interval);
            }
        }, 100);
    }

    presenter.setShowErrorsMode = function(){
        waitForElement(presenter.activateErrorsMode)
    };

    function displayIcon(area, isWrong) {
        var iconContainer = $('&lt;div class="icon-container"&gt;&lt;/div&gt;'),
            container = presenter.$view.find('.coloring-container'),
            containerWidth = container.width(),
            canvasWidth = $(presenter.canvas).width(),
            leftDistance = (containerWidth-canvasWidth)/ 2,
            position = $(presenter.canvas).position(),
            top = area.y + position.top - 5, // -5 because it's half of the icon container width and height
            left = area.x + leftDistance - 5;

        iconContainer.css({
            top: top + 'px',
            left: left + 'px'
        });

        iconContainer.addClass(isWrong ? 'wrong' : 'correct');

        container.append(iconContainer);
    }

    presenter.setWorkMode = function(){
        presenter.$view.find('.icon-container').remove();
        presenter.isShowErrorsModeActive = false;
    };

    presenter.clearCanvas = function() {
        presenter.ctx.clearRect(0, 0, presenter.canvasWidth, presenter.canvasHeight);
        presenter.ctx.drawImage(presenter.image[0], 0, 0);
    };

    presenter.reset = function(resetOnlyWrong){
        if (!presenter.isCanvasInitiated) {
            presenter.shouldResetAfterCanvasInit = true;
            presenter.runEnded.then(function() {
                _reset(resetOnlyWrong);
            })
        } else {
            _reset(resetOnlyWrong);
        }
    };

    function _reset(resetOnlyWrong) {
        if (resetOnlyWrong) {
            $.each(presenter.configuration.areas, function () {
                var area = this;
                if (!isCorrect(area)) {
                    presenter.clearArea(area.x, area.y, true)
                }
            });
        } else {
            presenter.clearCanvas();
        }
        presenter.removeAreaMark();
        presenter.removeColorList();
        presenter.$view.find('.icon-container').remove();
        presenter.isColored = false;
        presenter.isShowAnswersActive = false;
        presenter.isShowErrorsModeActive = false;
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);

        presenter.configuration.isDisabled = presenter.configuration.isDisabledByDefault;
        presenter.configuration.isDisabledByDefault ? presenter.disable() : presenter.enable();

        if (presenter.configuration.isErase) {
            presenter.configuration.currentFillingColor = presenter.configuration.lastUsedColor;
            presenter.configuration.isErase = false;
        } else {
            presenter.configuration.currentFillingColor = presenter.configuration.defaultFillingColor;
        }

        setColorsThatCanBeFilled();
        presenter.recolorImage();
        presenter.shouldResetAfterCanvasInit = false;
    }

    presenter.getErrorCount = function(){
        if (presenter.isShowAnswersActive) {
            return presenter.currentErrorCount;
        }

        if (presenter.configuration.isActivity &amp;&amp; presenter.imageHasBeenLoaded) {
            var errorsCount = 0;
            $.each(presenter.configuration.areas, function() {
                var area = this;

                if (!presenter.shouldBeTakenIntoConsideration(area)) {
                    return true; // continue
                }

                if (!isCorrect(area)) {
                    errorsCount++;
                }
            });
            return errorsCount;
        } else if (presenter.configuration.isActivity &amp;&amp; presenter.savedErrorCount) {
            return 0;
        } else {
            return 0;
        }
    };

    function isCorrect(area) {
        if (presenter.configuration.markTransparentAreas &amp;&amp; presenter.isAttempted() &amp;&amp; area.type === presenter.AREA_TYPE.TRANSPARENT) {
            return presenter.compareArrays(presenter.getColorAtPoint(area.x, area.y), area.defaultColor);
        }

        return presenter.compareArrays(presenter.getColorAtPoint(area.x, area.y), area.colorToFill);
    }

    presenter.shouldBeTakenIntoConsideration = function(area) {
        if (presenter.configuration.markTransparentAreas &amp;&amp; presenter.isAttempted() &amp;&amp; area.type === presenter.AREA_TYPE.TRANSPARENT) {
            return true;
        }

        return !presenter.compareArrays(presenter.getColorAtPoint(area.x, area.y), area.defaultColor);
    };

    presenter.getMaxScore = function(){
        if (presenter.configuration.isActivity) {
            var normalAreas = presenter.configuration.areas.filter(function (element) {
                   return (element.type == presenter.AREA_TYPE.NORMAL);
            });

            return normalAreas.length;
        } else {
            return 0;
        }
    };

    presenter.getScoreForNormalArea = function (area) {
        if (!presenter.shouldBeTakenIntoConsideration(area)) {
            return 0; // continue
        }

        if (isCorrect(area)) {
            return 1;
        }

        return 0;
    };

    presenter.getScoreForTransparentArea = function (area) {

        if(!presenter.compareArrays(presenter.getColorAtPoint(area.x, area.y), area.defaultColor)) {
            presenter.configuration.transparentAreaError = true;
        }

        return 0;
    };

    presenter.getScore = function(){
        if (presenter.isShowAnswersActive) {
            return presenter.currentScore;
        }

        if (presenter.configuration.isActivity &amp;&amp; presenter.imageHasBeenLoaded) {
            var scoreCount = 0;
            presenter.configuration.transparentAreaError = false;
            $.each(presenter.configuration.areas, function() {
                switch(this.type) {
                    case presenter.AREA_TYPE.NORMAL:
                        scoreCount += presenter.getScoreForNormalArea(this);
                        break;
                    case presenter.AREA_TYPE.TRANSPARENT:
                        scoreCount += presenter.getScoreForTransparentArea(this);
                        break;
                }
            });

            return scoreCount;
        } else if (presenter.configuration.isActivity &amp;&amp; presenter.savedScore) {
            return 0;
        } else {
            return 0;
        }
    };

    presenter.getState = function(){
        const wasAnswersShow = presenter.isShowAnswersActive;
        const lastAreaIdInGSAMode = presenter.currentAreaIdInGSAMode;

        if (!presenter.isCanvasInitiated &amp;&amp; presenter.initialState != null) {
            return presenter.initialState;
        }

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var filledAreas = [];
        $.each(presenter.configuration.areas, function() {
            if (presenter.shouldBeTakenIntoConsideration(this)) {
                filledAreas.push({
                    area: this,
                    color: presenter.getColorAtPoint(this.x, this.y)
                });
            }
        });

        var userAreas = [];
        if (presenter.configuration.userAreas != undefined) {
            userAreas = presenter.configuration.userAreas.map(function (elem) {
                return {
                    area: {x: elem.x, y: elem.y, type: elem.type, pixelPosition: elem.pixelPosition, colorToFill: elem.colorToFill},
                    color: elem.getColor()};
            });
        }


        var state = {
            filledAreas: filledAreas,
            currentFillingColor: presenter.configuration.currentFillingColor,
            isErase: presenter.configuration.isErase,
            colorsThatCanBeFilled: presenter.configuration.colorsThatCanBeFilled,
            isVisible: presenter.configuration.isVisible,
            isDisabled: presenter.configuration.isDisabled,
            isColored: presenter.isColored,
            score: presenter.getScore(),
            errorCount: presenter.getErrorCount(),
            userAreas: userAreas
        };

        if (wasAnswersShow) {
            presenter.currentAreaIdInGSAMode = lastAreaIdInGSAMode;
            presenter.showAnswersAgain();
        }

        return JSON.stringify(state);
    };

    presenter.upgradeState = function(state) {

        if (state.userAreas == undefined) {
            return presenter.upgradeUserAreas(state);
        }

        return state;
    };

    presenter.upgradeUserAreas = function(state) {
        var upgradedState = {};
        jQuery.extend(true, upgradedState, state); // Deep copy of model object

        if(state.userAreas == undefined) {
            upgradedState["userAreas"] = [];
        }

        return upgradedState;
    };

    presenter.restoreUserAreasFromState = function (state) {
        presenter.configuration.userAreas = [];

        $.each(state.userAreas, function() {
            var userArea = new areaObject(this.area.x, this.area.y, this.area.type);
            userArea.pixelPosition = this.area.pixelPosition;
            userArea.colorToFill = this.area.colorToFill;

            presenter.configuration.userAreas.push(userArea);
        });

    };

    presenter.getAreasToFillFromSetState = function (state) {
        var filledAreasArray = state.filledAreas;
        filledAreasArray = filledAreasArray.concat(state.userAreas);

        return filledAreasArray
    };

    presenter.setCurrentFillingColorInSetState = function (state) {
        if (presenter.configuration.isErase) {
            presenter.configuration.currentFillingColor = presenter.whiteRGBA;
        } else {
            presenter.configuration.currentFillingColor = state.currentFillingColor;
        }
    };

    presenter.setState = function(state){
        if (!presenter.isCanvasInitiated) {
            presenter.initialState = state;
        }

        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsed = JSON.parse(state);
        var upgradedState = presenter.upgradeState(parsed);

        presenter.configuration.isErase = upgradedState.isErase;
        presenter.configuration.isVisible = upgradedState.isVisible;
        presenter.configuration.isDisabled = upgradedState.isDisabled;
        presenter.isColored = upgradedState.isColored;
        presenter.savedScore = upgradedState.score;
        presenter.savedErrorCount = upgradedState.errorCount;

        presenter.setCurrentFillingColorInSetState(upgradedState);

        presenter.setVisibility(presenter.configuration.isVisible);

        presenter.restoreUserAreasFromState(upgradedState);

        var areasToFill = presenter.getAreasToFillFromSetState(upgradedState);

        if (upgradedState.colorsThatCanBeFilled) {
            presenter.restoreColoringAtState(areasToFill, upgradedState)
        }
    };

    presenter.restoreColoringAtState = function (filledAreasArray, state) {
        presenter.runEnded.then(function() {
            if (!presenter.shouldResetAfterCanvasInit) {
                presenter.configuration.colorsThatCanBeFilled = state.colorsThatCanBeFilled;
                $.each(filledAreasArray, presenter.restoreFilledArea);
            }
        });
    };

    presenter.restoreFilledArea = function (_, areaToFillObject) {
        presenter.floodFill(
            {
                x: areaToFillObject.area.x,
                y: areaToFillObject.area.y,
                color: presenter.getColorAtPoint(areaToFillObject.area.x, areaToFillObject.area.y)
            },
            areaToFillObject.color,
            presenter.configuration.tolerance
        );
        presenter.allColoredPixels = [];
    };

    presenter.floodFill = function (position, fillColor, tolerance) {
        var img = presenter.ctx.getImageData(0, 0, presenter.canvasWidth, presenter.canvasHeight),
            surface = img.data,
            length = surface.length,
            queue = [],
            x = position.x,
            y = position.y,
            targetColor = position.color,
            startingPixel = (x + y * presenter.canvasWidth) * 4,
            east = startingPixel, west = startingPixel, rightBound, leftBound, leftEdge = presenter.canvasWidth * 4;

        if(!pixelCompare(startingPixel, targetColor, fillColor, surface, length, tolerance).canFill) { return false; }

        queue.push(startingPixel);

        while(queue.length) {
            startingPixel = queue.pop();

            if(pixelCompareAndSet(startingPixel, targetColor, fillColor, surface, length, tolerance)) {
                east = startingPixel;
                west = startingPixel;
                leftBound = parseInt(startingPixel / leftEdge) * leftEdge; //left bound
                rightBound = leftBound + leftEdge;	//right bound
                while(leftBound &lt; (west -= 4) &amp;&amp; pixelCompareAndSet(west, targetColor, fillColor, surface, length, tolerance)); //go left until edge hit

                while(rightBound &gt; (east += 4) &amp;&amp; pixelCompareAndSet(east, targetColor, fillColor, surface, length, tolerance)); //go right until edge hit

                for(var j = west; j &lt; east; j += 4) {
                    if(j - leftEdge &gt;= 0 		&amp;&amp; pixelCompare(j - leftEdge, targetColor, fillColor, surface, length, tolerance).canFill) queue.push(j - leftEdge);
                    if(j + leftEdge &lt; length	&amp;&amp; pixelCompare(j + leftEdge, targetColor, fillColor, surface, length, tolerance).canFill) queue.push(j + leftEdge);
                }
            }
        }

        presenter.ctx.putImageData(img, 0, 0);
    };

    function pixelCompare(i, targetColor, fillColor, surface, length, tolerance) {
        if (i &lt; 0 || i &gt;= length) { // out of bounds

            return {
                canFill: false,
                withinTolerance: false
            };
        }
        if (surface[i + 3] === 0) { //surface is invisible

            return {
                canFill: false,
                withinTolerance: false
            };
        }

        if (targetColor[3] === fillColor[3] &amp;&amp; //target is same as fill
            targetColor[0] === fillColor[0] &amp;&amp;
            targetColor[1] === fillColor[1] &amp;&amp;
            targetColor[2] === fillColor[2]) {

            return {
                canFill: false,
                withinTolerance: false
            };
        }

        if (fillColor[3] === surface[i+3] &amp;&amp; // surface matches fillColor
            fillColor[0] === surface[i]  &amp;&amp;
            fillColor[1] === surface[i+1] &amp;&amp;
            fillColor[2] === surface[i+2]) {

            return {
                canFill: false,
                withinTolerance: false
            }

        }

        if (targetColor[3] === surface[i + 3] &amp;&amp; //target matches surface
            targetColor[0] === surface[i]  &amp;&amp;
            targetColor[1] === surface[i + 1] &amp;&amp;
            targetColor[2] === surface[i + 2]) {

            return {
                canFill: true,
                withinTolerance: false
            };
        }

        if (Math.abs(targetColor[3] - surface[i + 3]) &lt;= (255 - tolerance) &amp;&amp; //target to surface within tolerance
            Math.abs(targetColor[0] - surface[i]) &lt;= tolerance &amp;&amp;
            Math.abs(targetColor[1] - surface[i + 1]) &lt;= tolerance &amp;&amp;
            Math.abs(targetColor[2] - surface[i + 2]) &lt;= tolerance) {

            return {
                canFill: true,
                withinTolerance: true
            };

        }

        return {
            canFill: false,
            withinTolerance: false
        }; //no match
    }

    function pixelCompareAndSet(i, targetColor, fillColor, surface, length, tolerance) {
        var compareResult = pixelCompare(i, targetColor, fillColor, surface, length, tolerance);

        if (compareResult.canFill) {
            for(var j = i; j &lt; i + 4; j++) {
                presenter.allColoredPixels.push(j);
            }
            //fill the color
            surface[i]     = fillColor[0];
            surface[i + 1] = fillColor[1];
            surface[i + 2] = fillColor[2];
            surface[i + 3] = fillColor[3];

            return true;
        }

        return false;
    }

    presenter.onEventReceived = function (eventName, data) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }

        if (eventName == "GradualShowAnswers") {
            if (presenter.configuration.addonID == data.moduleID) {
                presenter.gradualShowAnswers();
            } else if (!presenter.isShowAnswersActive) {
                presenter.activateShowAnswersMode();
            }
        }

        if (eventName == "GradualHideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function () {
        if (!presenter.configuration.isActivity) {
            return;
        }

        if (!presenter.isShowAnswersActive) {
            presenter.activateShowAnswersMode();
        }

        var areas = presenter.configuration.areas;
        for (var i=0; i&lt; areas.length; i++) {
            presenter.fillAreaWithCorrectColor(areas[i]);
        }
    };

    presenter.gradualShowAnswers = function () {
        if (!presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            if (!presenter.configuration.isActivity) {
                return;
            }

            if (!presenter.isShowAnswersActive) {
                presenter.activateShowAnswersMode();
            }

            var area = presenter.configuration.areas[presenter.currentAreaIdInGSAMode];
            presenter.fillAreaWithCorrectColor(area);
            presenter.currentAreaIdInGSAMode++;
        } else {
            presenter.showAnswers();
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.configuration.isActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.deactivateShowAnswersMode();
    };

    presenter.showAnswersAgain = function () {
        if (!presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            if (!presenter.isShowAnswersActive) {
                presenter.activateShowAnswersMode();
            }

            if (presenter.currentAreaIdInGSAMode === 0) {
                presenter.showAnswers();
            } else {
                for (let i = 0; i &lt; presenter.currentAreaIdInGSAMode; i++) {
                    const area = presenter.configuration.areas[i];
                    presenter.fillAreaWithCorrectColor(area);
                }
            }
        } else {
            presenter.showAnswers();
        }
    }

    presenter.activateShowAnswersMode = function () {
        presenter.isShowErrorsModeActive = false;

        presenter.$view.find('.icon-container').remove();
        presenter.currentScore = presenter.getScore();
        presenter.currentErrorCount = presenter.getErrorCount();

        presenter.backupUserAreas();

        presenter.clearCanvas();
        presenter.recolorImage();

        presenter.isShowAnswersActive = true;
    };

    presenter.deactivateShowAnswersMode = function () {
        presenter.currentAreaIdInGSAMode = 0;

        presenter.clearCanvas();
        presenter.recolorImage();

        presenter.restoreUserAreas();

        presenter.isShowAnswersActive = false;
    };

    presenter.backupUserAreas = function () {
        presenter.tmpFilledAreas = [];
        $.each(presenter.configuration.areas, function() {
            if (presenter.shouldBeTakenIntoConsideration(this)) {
                presenter.tmpFilledAreas.push({
                    area: this,
                    color: presenter.getColorAtPoint(this.x, this.y)
                });
            }
        });

        if (presenter.configuration.userAreas) {
            for (var i = 0; i &lt; presenter.configuration.userAreas.length; i++) {
                var area = presenter.configuration.userAreas[i];
                presenter.tmpFilledAreas.push({
                    area: area,
                    color: area.getColor()
                });
            }
        }
    };

    presenter.restoreUserAreas = function () {
        $.each(presenter.tmpFilledAreas, function() {
            presenter.floodFill({
                    x: this.area.x,
                    y: this.area.y,
                    color: presenter.whiteRGBA
                },
                this.color,
                presenter.configuration.tolerance);

            presenter.allColoredPixels = [];
        });
    };

    presenter.fillAreaWithCorrectColor = function (area) {
        presenter.floodFill({
                x: area.x,
                y: area.y,
                color: presenter.whiteRGBA
            },
            [area.colorToFill[0], area.colorToFill[1], area.colorToFill[2], area.colorToFill[3]],
            presenter.configuration.tolerance);

        presenter.allColoredPixels = [];
    };

    presenter.getActivitiesCount = function () {
        if (presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            return 1;
        }
        return presenter.configuration.areas.length;
    };

    presenter.getRGBAStringFromRGBAArray = function(rgbaArray) {
        return rgbaArray.reduce((prev, curr) =&gt; (prev === "") ? curr : `${prev} ${curr}`, "");
    }

    function scalePoint({x, y}) {
        var scaledPoint = {x: x, y: y};
        if (!presenter.playerController)
            return scaledPoint;

        var scale = presenter.playerController.getScaleInformation();
        if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
            scaledPoint.x = Math.floor(scaledPoint.x / scale.scaleX);
            scaledPoint.y = Math.floor(scaledPoint.y / scale.scaleY);
        }
        return scaledPoint;
    }

    function ColoringKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    presenter.isDeactivationBlocked = function() {
        if (presenter.isTTSExitBlocked) {
            presenter.isTTSExitBlocked = false;
            return true;
        }
        return false;
    };

    ColoringKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    ColoringKeyboardController.prototype.isColorSelectActive = false;
    ColoringKeyboardController.prototype.lastAreaElement = null;
    ColoringKeyboardController.prototype.colorElementsMap = {};
    ColoringKeyboardController.prototype.lastAreaElementIndex = null;
    ColoringKeyboardController.prototype.constructor = ColoringKeyboardController;


    presenter.buildKeyboardController = function AddonColoring_buildKeyboardController () {
        const elements = presenter.getElementsForKeyboardNavigation();
        presenter.keyboardControllerObject = new ColoringKeyboardController(elements, 1);
    };

    presenter.keyboardController = function AddonColoring_keyboardController (keycode, isShiftKeyDown, event) {
        if (presenter.shouldIgnoreKeyNav()) {
            const enterKeycode = 13;
            if (keycode === enterKeycode) presenter.keyboardControllerObject.escape(event);
            event.preventDefault();
            return;
        }
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    presenter.shouldIgnoreKeyNav = function () {
        return !presenter.configuration.colors?.length &amp;&amp; !presenter.isTTS();
    }

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.configuration.areas ? presenter.configuration.areas : [];
    };

    presenter.getAvailableColorsForKeyboardNavigation = function() {
        return presenter.configuration.colors;
    };

    ColoringKeyboardController.prototype.mark = function (element) {
        if (!this.isColorSelectActive) return;
        const $colorElement = this.colorElementsMap[this.keyboardNavigationCurrentElement.speechText];
        if ($colorElement) {
            $colorElement.addClass('keyboard_navigation_active_element');
        }
    };
    ColoringKeyboardController.prototype.unmark = function (element) {
        if (!this.isColorSelectActive) return;
        const $colorElement = this.colorElementsMap[this.keyboardNavigationCurrentElement.speechText];
        if ($colorElement) {
            $colorElement.removeClass('keyboard_navigation_active_element');
        }
    };

    ColoringKeyboardController.prototype.nextRow = function (event) {
        this.nextElement(event);
    }

    ColoringKeyboardController.prototype.previousRow = function (event) {
        this.previousElement(event);
    }

    ColoringKeyboardController.prototype.nextElement = function (event) {
        if (event) event.preventDefault();
        if (this.keyboardNavigationCurrentElementIndex === this.keyboardNavigationElements.length - 1) {
            this.readCurrentElement();
        } else {
            this.switchElement(1);
        }
    }

    ColoringKeyboardController.prototype.previousElement = function (event) {
        if (event) event.preventDefault();
        if (this.keyboardNavigationCurrentElementIndex === 0) {
            this.readCurrentElement();
        } else {
            this.switchElement(-1);
        }
    }

    ColoringKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        if (this.isColorSelectActive) {
            this.adjustScroll(move);
        } else {
            presenter.markAreaWithCircle(this.keyboardNavigationCurrentElement);
        }
        this.readCurrentElement();
    };

    ColoringKeyboardController.prototype.adjustScroll = function (move) {
        let currentElement = this.colorElementsMap[this.keyboardNavigationCurrentElement.speechText][0];
        let parentElement = currentElement.parentElement;

        if (!isScrollbarVisible(parentElement)) {
            return;
        }

        if (move &lt; 0) {
            parentElement.scrollTo(0, currentElement.offsetTop);
        } else {
            let currentElementOffsetBottom = currentElement.offsetTop + currentElement.offsetHeight;
            if (currentElementOffsetBottom &gt; parentElement.offsetHeight) {
                parentElement.scrollTo(0, currentElementOffsetBottom - parentElement.offsetHeight);
            } else {
                parentElement.scrollTo(0, 0);
            }
        }
    }

    function isScrollbarVisible(element) {
        return element.scrollHeight &gt; element.clientHeight;
    }

    ColoringKeyboardController.prototype.enter = function (event) {
        if (presenter.isFirstEnter) {
            this.setElements.call(this, presenter.getElementsForKeyboardNavigation());
            presenter.isFirstEnter = false;
        }
        KeyboardController.prototype.enter.call(this, event);
        if (!this.isColorSelectActive) {
            presenter.markAreaWithCircle(this.keyboardNavigationCurrentElement);
        }
        this.readCurrentElement();
    };

    ColoringKeyboardController.prototype.select = function (event) {
        if (event) event.preventDefault();
        if (!this.isSelectEnabled) return;
        if (presenter.isShowAnswersActive) return;
        if (presenter.isShowErrorsModeActive) return;
        if (!presenter.configuration.colors?.length) return;

        if (this.isColorSelectActive) {
            presenter.removeColorList();
            this.colorAreaWithSelectedColor();
            this.readSelectedColor();
            this.switchElementsToAreas();
            presenter.isColored = true;
        } else {
            this.switchElementsToColors();
            this.readCurrentElement();
        }
    };

    presenter.markAreaWithCircle = function ({ x, y }) {
        presenter.removeAreaMark();
        const $mark = $("&lt;span&gt;&lt;/span&gt;");
        $mark.addClass("coloring-circle-mark");
        $mark.css({
            'top' : y + 20,
            'left' : x + 20,
        });
        const coloringWrapper = presenter.$view.find('.coloring-wrapper');
        coloringWrapper.append($mark);
        presenter.$areaMark = $mark;
    }

    presenter.removeAreaMark = function () {
        if (presenter.$areaMark) {
            presenter.$areaMark.remove();
        }
    }

    presenter.removeColorList = function () {
      if (presenter.$colorList) {
          presenter.$colorList.remove();
      }
    }

    ColoringKeyboardController.prototype.colorAreaWithSelectedColor = function () {
            const previousColor = presenter.configuration.currentFillingColor;
            const colorString = presenter.getRGBAStringFromRGBAArray(this.keyboardNavigationCurrentElement.colorRGBA);
            presenter.setColor(colorString);
            presenter.floodFill({
                    x: this.lastAreaElement.x,
                    y: this.lastAreaElement.y,
                    color: presenter.getColorAtPoint(this.lastAreaElement.x, this.lastAreaElement.y)
                },
                this.keyboardNavigationCurrentElement.colorRGBA,
                presenter.configuration.tolerance
            );
            presenter.configuration.currentFillingColor = previousColor;
            this.sendColoredEvent(colorString);
    }

    ColoringKeyboardController.prototype.sendColoredEvent = function (colorString) {
            const isColorWhite = colorString === presenter.getRGBAStringFromRGBAArray(presenter.whiteRGBA);
            setTimeout(() =&gt;{
                // Without timeout there are issues on Firefox if event handling takes too long
                presenter.sendEvent(
                    [this.lastAreaElement.x, this.lastAreaElement.y],
                    isColorWhite ? 0 : 1,
                    isCorrect(this.lastAreaElement) ? 1 : 0
                );
            }, 0);
    }

    ColoringKeyboardController.prototype.switchElementsToAreas = function () {
        this.isColorSelectActive = false;
        this.setElements(presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.switchElement.call(this, this.lastAreaElementIndex);
    }

    ColoringKeyboardController.prototype.createColorListForKeyNav = function () {
        const $colorList = $("&lt;span&gt;&lt;/span&gt;");
        $colorList.addClass("coloring-color-list");
        $colorList.css({
            'top': this.lastAreaElement.y,
            'left': this.lastAreaElement.x,
        });
        this.populateColorListWithColors($colorList);
        const coloringWrapper = presenter.$view.find('.coloring-wrapper');
        coloringWrapper.append($colorList);
        presenter.$colorList = $colorList;
    }

    ColoringKeyboardController.prototype.populateColorListWithColors = function ($colorList) {
        const colors = presenter.configuration.colors;

        colors.forEach(color =&gt; {
            const $color = $("&lt;div&gt;&lt;/div&gt;");
            $color.addClass("coloring-color-list-element");
            $color.text(color.speechText);
            $colorList.append($color);
            this.colorElementsMap[color.speechText] = $color;
        });
    }

    ColoringKeyboardController.prototype.switchElementsToColors = function () {
        this.isColorSelectActive = true;
        this.lastAreaElement = this.keyboardNavigationCurrentElement;
        this.lastAreaElementIndex = this.keyboardNavigationCurrentElementIndex;
        this.createColorListForKeyNav();
        const colors = presenter.getAvailableColorsForKeyboardNavigation();
        this.setElements(colors);
    }

    ColoringKeyboardController.prototype.readCurrentElement = function () {
        const text = this.keyboardNavigationCurrentElement.speechText;
        const textWithPrefix = this.isColorSelectActive ? getColorTTS(text) : this.getAreaTTS(text);

        presenter.speak(textWithPrefix);
    };

    ColoringKeyboardController.prototype.readSelectedColor = function () {
        const textVoiceObject = [];

        const color = this.keyboardNavigationCurrentElement.speechText;

        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.selected);
        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, color);

        presenter.speak(textVoiceObject);
    };

    ColoringKeyboardController.prototype.escape = function (event) {
        if (event) {
            event.stopPropagation();
            event.preventDefault();
        }
        if (this.isColorSelectActive) {
            presenter.isTTSExitBlocked = true;
            this.switchElementsToAreas();
            presenter.removeColorList();
            this.readCurrentElement();
        } else {
            this.exitWCAGMode();
        }
    };

    ColoringKeyboardController.prototype.exitWCAGMode = function () {
        presenter.removeAreaMark();
        presenter.removeColorList();
        this.isColorSelectActive = false;
        presenter.isFirstEnter = true;
        this.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    function getColorTTS(colorText) {
        const textVoiceObject = [];

        const colorPrefix = presenter.speechTexts.color;
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, colorPrefix);

        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, colorText);

        return textVoiceObject;
    }

    ColoringKeyboardController.prototype.getAreaTTS = function(areaText) {
        const textVoiceObject = [];

        const areaPrefix = presenter.speechTexts.area;
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, areaPrefix);

        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, areaText);

        const colorText = this.getCurrentAreaColorSpeechText();
        if (colorText) {
            if (colorText !== presenter.transparentAreaTTS) {
                pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.color);
                pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, colorText);
            }

            if (presenter.isShowErrorsModeActive) {
                if (isCorrect(this.keyboardNavigationCurrentElement)) {
                    pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.correct);
                } else {
                    pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.incorrect);
                }
            }
        }

        return textVoiceObject;
    }

    ColoringKeyboardController.prototype.getCurrentAreaColorSpeechText = function () {
        const colorArray = presenter.getColorAtPoint(
            this.keyboardNavigationCurrentElement.x,
            this.keyboardNavigationCurrentElement.y
        );
        const colorString = presenter.getRGBAStringFromRGBAArray(colorArray);
        const isAreaTransparent = this.keyboardNavigationCurrentElement.type === presenter.AREA_TYPE.TRANSPARENT &amp;&amp;
            presenter.isColorSimilar(colorArray, presenter.whiteRGBA, 2)

        if (presenter.configuration.markTransparentAreas &amp;&amp; presenter.isAttempted() &amp;&amp; isAreaTransparent) {
            return presenter.transparentAreaTTS;
        }

        return presenter.colorSpeechTextMap[colorString];
    }

    presenter.isColorSimilar = function (currentArea, defaultArea, tolerance = 0) {
        if (currentArea.length !== defaultArea.length) {
            return false;
        }

        const differences = [];
        currentArea.forEach((colorValue, index) =&gt; {
            differences.push(Math.abs(colorValue - defaultArea[index]));
        })

        return differences.every(differ =&gt; differ &lt;= tolerance);
    }

    function pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, message) {
        textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    function pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, message) {
        textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
    }

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.speak = function(text) {
        const tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(text);
        }
    };

    presenter.isTTS = function () {
        return presenter.getTextToSpeechOrNull(presenter.playerController) &amp;&amp; presenter.isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function Coloring_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}

AddonColoring_create.__supported_player_options__ = {
    resetInterfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Completion_Progress" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Turn off automatic counting" nameLabel="Completion_Progress_property_turn_off_automatic_counting" type="boolean"/>
    </model>
<css>.addon_Completion_Progress {
    padding: 2px;
    width: 130px;
    height: 25px;
    border-radius: 5px;
    border: 2px solid black;
}

.addon_Completion_Progress .progress-bar {
    background-color: #FA8805;
    border-radius: 5px;
    height: 100%;
}

.addon_Completion_Progress .progress-text {
    color: #000000;
    font-size: 18px;
    position: absolute;
    width: 10%;
    left: 45%;
    height: 50%;
    top: 25%;
    text-align: center;
}
</css><view>&lt;div class="progress-bar" style="width: 0;"&gt;&lt;/div&gt;
&lt;div class="progress-text"&gt;0%&lt;/div&gt;
</view><preview>&lt;div class="progress-bar" style="width: 45%;"&gt;&lt;/div&gt;
&lt;div class="progress-text"&gt;45%&lt;/div&gt;
</preview><presenter>function AddonCompletion_Progress_create() {
    var presenter = function () { };

    var playerController;
    var eventBus;

    presenter.currentProgress = 0;
    presenter.modules = [];

    presenter.setPlayerController = function(controller) {
        playerController = controller;
        eventBus = controller.getEventBus();
        eventBus.addEventListener('PageLoaded', this);

        presenter.page = controller.getPresentation().getPage(controller.getCurrentPageIndex());
    };

    presenter.updateProgress = function () {
        if (presenter.modules.length == 0) {
            presenter.currentProgress = 0;
        } else {
            var attemptedCount = 0;

            for (var i = 0; i &lt; presenter.modules.length; i++) {
                if (presenter.modules[i].isAttempted()) {
                    attemptedCount++;
                }
            }

            presenter.currentProgress = Math.floor((attemptedCount / presenter.modules.length) * 100);
        }

        presenter.updateProgressUI(presenter.currentProgress);
    };

    presenter.loadModules = function () {
        if (!presenter.page.isReportable()) {
            return;
        }

        var modules = presenter.page.getModulesAsJS(),
            module, loadedModules = [];

        for (var i = 0; i &lt; modules.length; i++) {
            module = playerController.getModule(modules[i]);

            if (module &amp;&amp; module.isAttempted !== undefined) {
                loadedModules.push(module);
            }
        }

        presenter.modules = loadedModules;
    };

    presenter.validateModel = function (model) {
        return {
            automaticCounting: !ModelValidationUtils.validateBoolean(model['Turn off automatic counting']),
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible'])
        };
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();

        presenter.$view = $(view);
        presenter.model = model;
        presenter.configuration = presenter.validateModel(model);

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        if (!isPreview &amp;&amp; presenter.configuration.automaticCounting) {
            eventBus.addEventListener('ValueChanged', this);
            presenter.pageLoaded.then(function() {
                presenter.loadModules();
                presenter.updateProgress();
            });
        }
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ValueChanged") {
            presenter.updateProgress();
        }
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        }
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };
    
    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };


    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            currentProgress: presenter.currentProgress
        });
    };

    presenter.setState = function (state) {
       if (!state) return;

        var parsedState = JSON.parse(state);

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);

        presenter.currentProgress = parsedState.currentProgress;
        presenter.updateProgressUI(presenter.currentProgress);
    };
    
    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'setProgress': presenter.setProgressCommand,
            'getProgress': presenter.getProgress
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.updateProgressUI = function (progress) {
        presenter.$view.find('.progress-bar').css('width', progress + '%');
        presenter.$view.find('.progress-text').text(progress + '%');
    };

    presenter.reset = function () {
        presenter.currentProgress = 0;

        presenter.updateProgressUI(0);
    };

    presenter.getProgress = function () {
        return presenter.currentProgress;
    };

    presenter.setProgress = function (progress) {
        var validatedProgress = ModelValidationUtils.validateIntegerInRange(progress, 100);

        if (!validatedProgress.isValid) {
            return;
        }

        presenter.currentProgress = validatedProgress.value;
        presenter.updateProgressUI(presenter.currentProgress);
    };

    presenter.setProgressCommand = function (params) {
        presenter.setProgress(params[0]);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="ConnectingDots" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property displayName="Dots' coordinates" name="Dots" nameLabel="ConnectingDots_property_dots" type="text"/>
		<property displayName="Dots' indexes" name="Indexes" nameLabel="ConnectingDots_property_indexes" type="text"/>
		<property name="Is activity" nameLabel="ConnectingDots_property_is_activity" type="boolean"/>
		<property displayName="Animation time" name="Time" nameLabel="ConnectingDots_property_time" type="string"/>
		<property displayName="Start image" name="Image A" nameLabel="ConnectingDots_property_image_a" type="image"/>
		<property displayName="End image" name="Image B" nameLabel="ConnectingDots_property_image_b" type="image"/>
		<property name="Is disabled" nameLabel="ConnectingDots_property_is_disabled" type="boolean"/>
		<property name="Show all answers in gradual show answers mode" nameLabel="ConnectingDots_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
	</model>
<css>.connectingdots {
    border:3px solid #ccc;
    position:relative;
    -ms-touch-action: none;
    touch-action: none;
}

.connectingdots .dot_container {
    position:absolute;
    min-height: 25px;
    min-width: 25px;
}

.connectingdots .dot {
    position:relative;
    height:10px;
    width:10px;
    background-color:#000;
    border-radius:5px;
    float:left;
    margin:-5px -5px -5px -5px;
}

.connectingdots .image-start {
    position:absolute;
    width: 100%;
    height: 100%;
}
.connectingdots .image-end {
    position:absolute;
    width: 100%;
    height: 100%;
}
.connectingdots .dot_number {
    position:relative;
    float:left;
    cursor: default;
}

.connectingdots .line {
    height:1px;
    position:absolute;
    background-color:#000;
}
.connectingdots .line-show-answer {
    height:1px;
    position:absolute;
    background-color:gray;
}

.connectingdots .active .dot {
    background-color:#146CDD;
}

.connectingdots .active .dot_number {
    color:#146CDD;
}

.connectingdots .correct {
    border:3px solid #04B431;
}

.connectingdots .wrong {
    border:3px solid #DF0101;
}
</css><view>&lt;div class="connectingdots"&gt;&lt;/div&gt;
</view><preview>&lt;div class="connectingdots"&gt;
&lt;/div&gt;
</preview><presenter>function AddonConnectingDots_create(){
    var presenter = function(){};

    presenter.toSelect = 0;
    presenter.lineIds = new Array();
    presenter.activity = false;
    presenter.error = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;

    presenter.ERROR_CODES = {
        'PE': 'Points coordinates incorrect!',
        'PO': 'Points outside the addon!',
        'NP': 'Define the points!',
        'IE': 'Indexes incorrect!'
    };

    presenter.onEventReceived = function (eventName, data) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                if (!presenter.isGradualShowAnswersActive) {
                    presenter.isGradualShowAnswersActive = true;
                }
                if (data.moduleID === presenter.addonID) {
                    presenter.gradualShowAnswers(parseInt(data.item, 10));
                }
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    function checkIndexes(indexes,numberOfPoints) {
        indexes = indexes.replace(/\s/g, '');
        pointsIndexes = new Array(numberOfPoints);
        if (indexes == '') {
            for (var i = 0; i &lt; numberOfPoints; i++) {
                pointsIndexes[i] = i+1;
            }
            return pointsIndexes;
        } else if (indexes[0] == '*' &amp;&amp; !(isNaN(indexes.substring(1)))){
            for (var i = 0; i &lt; numberOfPoints; i++) {
                pointsIndexes[i] = (i+1)*indexes.substring(1);
            }
            return pointsIndexes;
        } else {
            pointsIndexes = indexes.split(',');
            if (pointsIndexes.length != numberOfPoints) {
                presenter.error = 'IE';
                return false;
            } else {
                return pointsIndexes;
            }
        }
    }
    function getPoint(con,coords) {
        coords = coords.replace(/\s/g, '');
        if (coords == '') {
            presenter.error = 'NP';
            return false;
        } else {
            var points = coords.split(']');
            var numberOfPoints = points.length - 1;
            var pointsCoordinates = new Array(numberOfPoints);
            var tmp_dane;
            if (points[numberOfPoints] != '') {
                presenter.error = 'PE';
                return false;
            }
            var i;
            for (i = 0; i&lt;points.length-1; i++) {
                tmp_dane = points[i].split(',');
                pointsCoordinates[i] = new Array(2);
                if (tmp_dane[0][0] != '[') {
                    presenter.error = 'PE';
                    return false;
                }
                pointsCoordinates[i][0] = tmp_dane[0].substring(1);
                pointsCoordinates[i][1] = tmp_dane[1];
                if (isNaN(pointsCoordinates[i][0]) || isNaN(pointsCoordinates[i][1])) {
                    presenter.error = 'PE';
                    return false;
                } else if (pointsCoordinates[i][0] &gt;= (con.width()) || pointsCoordinates[i][0] &lt;= 0 || pointsCoordinates[i][1] &lt;= 0 || pointsCoordinates[i][1] &gt;= (con.height())) {
                    presenter.error = 'PO';
                    return false;
                }
                pointsCoordinates[i][1] = parseInt(pointsCoordinates[i][1],10);
                pointsCoordinates[i][0] = parseInt(pointsCoordinates[i][0],10);
            }
        }
        return pointsCoordinates;
    }

    presenter.drawPoints = function() {
        for(var i=0; i&lt;(presenter.points).length; i++) {
            div = $('&lt;div id="dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+i+'" order_value="'+i+'" class="dot_container" style="left: '+presenter.points[i][0]+'px; top: '+presenter.points[i][1]+'px;"&gt;&lt;div class="dot"&gt;&lt;/div&gt;&lt;div class="dot_number"&gt;'+presenter.indexes[i]+'&lt;/div&gt;&lt;/div&gt;');
            presenter.$view.find('.connectingdots').append(div);
        }
    };

    presenter.drawLine = function(i, time, fade, showAnswer) {
        var m, angle, d, transform, id, line, x1, y1, x2, y2;
        x1 = parseInt(presenter.points[i-1][0],10);
        y1 = parseInt(presenter.points[i-1][1],10);
        x2 = parseInt(presenter.points[i][0],10);
        y2 = parseInt(presenter.points[i][1],10);
        m = (y2-y1)/(x2-x1);
        angle = (Math.atan(m))*180/(Math.PI);
        d = Math.sqrt(((x2-x1)*(x2-x1)) + ((y2-y1)*(y2-y1)));

        if (x2 &gt;= x1){
            transform = (360 + angle) % 360;
        } else {
            transform = 180 + angle;
        }

        line = document.createElement("div");
        line.style = `left: ${x1}px; top: ${y1}px`;
        line.innerHTML = "&amp;nbsp;";
        if (showAnswer) {
            id ='line_' + i + '_' + new Date().getTime();
            line.classList.add("line-show-answer");
        } else {
            id ='line_' + new Date().getTime();
            line.classList.add("line");
            presenter.lineIds[i] = id;
        }
        line.id = id;
        presenter.$view.find('.connectingdots').append(line);
        $(line).css({
            'left': x1,
            'top': y1,
            'width': '0px',
            'transform' : 'rotate('+transform+'deg)',
            'transform-origin' : '0px 0px',
            '-ms-transform' : 'rotate('+transform+'deg)',
            '-ms-transform-origin' : '0px 0px',
            '-moz-transform' : 'rotate('+transform+'deg)',
            '-moz-transform-origin' : '0px 0px',
            '-webkit-transform' : 'rotate('+transform+'deg)',
            '-webkit-transform-origin' : '0px 0px',
            '-o-transform' : 'rotate('+transform+'deg)',
            '-o-transform-origin' : '0px 0px'
        });

        $(line).animate({
            width: d
        }, time, "linear", function(){
            if (i == (presenter.points).length-1 &amp;&amp; !showAnswer) {
                presenter.$view.find('.image-end').remove();
                if (presenter.imageSrcEnd != '') {
                    if (fade) {
                        presenter.$view.find('.image-start').fadeOut('slow');
                    } else {
                        presenter.$view.find('.image-start').remove();
                    }
                    var image2 = document.createElement('img');
                    $(image2).attr('src', presenter.imageSrcEnd);
                    $(image2).addClass('image-end');
                    presenter.$view.find('.connectingdots').prepend(image2);
                    if (fade) {
                        presenter.$view.find('.image-end').hide();
                        presenter.$view.find('.image-end').fadeIn('slow');
                    }
                }
            }
        });
    };

    presenter.drawTempLine = function(i, x, y) {
        if (presenter.draw !== false) {
            var scaledPoint = scalePoint({x: x, y: y});
            x = scaledPoint.x;
            y = scaledPoint.y;

            if (presenter.$view.find('#line_tmp').length &gt; 0) {
                presenter.$view.find('#line_tmp').remove();
            }
            var m, angle, d, transform, x1, y1;
            x1 = parseInt(presenter.points[i][0],10);
            y1 = parseInt(presenter.points[i][1],10);
            m = (y-y1)/(x-x1);
            angle = (Math.atan(m))*180/(Math.PI);
            d = Math.sqrt(((x-x1)*(x-x1)) + ((y-y1)*(y-y1)));
            if (x &gt;= x1){
                transform = (360 + angle) % 360;
            } else {
                transform = 180 + angle;
            }

            const div = $('&lt;div&gt;');
            div.attr('id','line_tmp');
            div.attr('class','line');
            div.attr('style','left: '+x1+'px; top: '+y1+'px');
            presenter.$view.find('.connectingdots').append(div);
            presenter.$view.find('#line_tmp').css({
                'left': x1,
                'top': y1,
                'width': d,
                'transform' : 'rotate('+transform+'deg)',
                'transform-origin' : '0px 0px',
                '-ms-transform' : 'rotate('+transform+'deg)',
                '-ms-transform-origin' : '0px 0px',
                '-moz-transform' : 'rotate('+transform+'deg)',
                '-moz-transform-origin' : '0px 0px',
                '-webkit-transform' : 'rotate('+transform+'deg)',
                '-webkit-transform-origin' : '0px 0px',
                '-o-transform' : 'rotate('+transform+'deg)',
                '-o-transform-origin' : '0px 0px'
            });
        }
    };

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
        }
    };

    presenter.hide = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.disable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isDisabled = true;
    };

    presenter.enable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isDisabled = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        if(presenter.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };

    presenter.isAttempted = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        return (!(presenter.activity) || (presenter.toSelect &gt; 0));
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(selected,mark) {
        var score = 0;
        if ((presenter.points).length == selected &amp;&amp; selected == presenter.toSelect) {
            score = 1;
        }
        return {
            source : presenter.addonID,
            item : selected,
            value : mark,
            score : score
        };
    };
    presenter.triggerPointSelectedEvent = function(selected,mark) {
        var eventData = presenter.createEventData(selected,mark);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.run = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.randomId = Math.floor(100000*Math.random());
        presenter.$view = $(view);
        presenter.addonID = model.ID;
        presenter.model = model;
        distance = 0;
        var con = presenter.$view.find('.connectingdots').parent();
        presenter.$view.find('.connectingdots').css({
            'width': con.width(),
            'height': con.height()
        });
        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        var coords = presenter.model['Dots'];
        presenter.time = presenter.model['Time'];
        presenter.activity = ModelValidationUtils.validateBoolean(presenter.model['Is activity']);

        presenter.imageSrcStart = presenter.model['Image A'];
        presenter.imageSrcEnd = presenter.model['Image B'];

        presenter.points = getPoint(con,coords);
        presenter.indexes = checkIndexes(presenter.model['Indexes'],(presenter.points).length);
        presenter.isDisabled = ModelValidationUtils.validateBoolean(presenter.model['Is disabled']);
        presenter.showAllAnswersInGradualShowAnswersMode = ModelValidationUtils.validateBoolean(presenter.model["Show all answers in gradual show answers mode"]);
        presenter.initIsDisabled = presenter.isDisabled;

        var image1 = document.createElement('img');
        $(image1).attr('src', presenter.imageSrcStart);
        $(image1).addClass('image-start');

        var image2 = document.createElement('img');
        $(image2).attr('src', presenter.imageSrcEnd);
        $(image2).addClass('image-end');

        presenter.draw = false;
        presenter.isDown = false;

        if (presenter.time == '') {
            presenter.time = 0;
        }
        var $div = presenter.$view.find('.connectingdots');
        var Width = $div.width();
        var Height = $div.height();
        var tmpTime;
        //var Left = parseInt(con[0].offsetLeft,10);
        //var Top = parseInt(con[0].offsetTop,10);
        var Left = parseInt($div.offset().left,10);
        var Top = parseInt($div.offset().top,10);

        if (presenter.points == false || presenter.indexes == false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
        } else {
            presenter.drawPoints();
            if (presenter.imageSrcStart != '') {
                presenter.$view.find('.connectingdots').prepend(image1);
            }

            $(view).find('.connectingdots').click(function(event){
                event.stopPropagation();
                presenter.isDown = false;
            });

            presenter.$view.find('.dot').on('mousedown', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowingAnswers()) {
                    presenter.isDown = false;
                }
            });

            presenter.$view.find('.dot_number').on('mousedown', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowingAnswers()) {
                    presenter.isDown = false;
                }
            });

            presenter.$view.on('mousedown', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowingAnswers()) {
                    presenter.isDown = true;
                }
            });

            presenter.$view.on('mousemove',function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.draw = presenter.toSelect - 1;
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; presenter.draw !== -1 &amp;&amp; !presenter.isShowAnswersActive) {
                    if (presenter.draw &lt; (presenter.points).length-1 &amp;&amp; presenter.isDown === true) {
                        presenter.mouseSX = parseInt(e.pageX,10) - parseInt($div.offset().left,10);
                        presenter.mouseSY = parseInt(e.pageY,10) - parseInt($div.offset().top,10);
                        presenter.drawTempLine(presenter.draw,presenter.mouseSX,presenter.mouseSY);
                    }
                }
            });

            $(view).find('.dot_container').on('mouseup',function(e){
                e.stopPropagation();
                e.preventDefault();
                if (presenter.$view.find('#line_tmp').length &gt; 0) {
                    presenter.$view.find('#line_tmp').remove();
                }
                if (!presenter.isErrorMode &amp;&amp; !presenter.isDisabled &amp;&amp; !presenter.isShowAnswersActive) {
                    i = parseInt($(this).attr('order_value'),10);

                    if (i == presenter.toSelect){
                        if (i &gt; 0) {
                            presenter.$view.find('div#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(i-1)).removeClass('active');
                            if (presenter.isDown) {
                                presenter.drawLine(i,0,true);
                            } else {
                                presenter.drawLine(i,presenter.time,true);
                            }
                        }

                        if (i &lt; (presenter.points).length-1) {
                            presenter.$view.find('div#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(i)).addClass('active');
                        }

                        presenter.toSelect++;
                        presenter.triggerPointSelectedEvent(i+1,1);
                    } else {
                        presenter.triggerPointSelectedEvent(i+1,0);
                    }
                }
                presenter.isDown = false;
            });

            presenter.$view.on('mouseleave mouseup',function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.draw = false;
                presenter.isDown = false;

                if (presenter.$view.find('#line_tmp').length &gt; 0) {
                    presenter.$view.find('#line_tmp').remove();
                }
            });


            presenter.$view.on('touchstart', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    presenter.isDown = true;
                    presenter.mouseX = parseInt(e.originalEvent.touches[0].pageX,10) - parseInt($div.offset().left,10);
                    presenter.mouseY = parseInt(e.originalEvent.touches[0].pageY,10) - parseInt($div.offset().top,10);
                }
            });
            presenter.$view.on('touchmove', function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.mouseX = parseInt(e.originalEvent.touches[0].pageX,10) - parseInt($div.offset().left,10);
                presenter.mouseY = parseInt(e.originalEvent.touches[0].pageY,10) - parseInt($div.offset().top,10);
                if (presenter.draw !== false &amp;&amp; presenter.mouseX &lt;  Width &amp;&amp; presenter.mouseY &lt; Height &amp;&amp; presenter.mouseX &gt; 0 &amp;&amp; presenter.mouseY &gt; 0 &amp;&amp; !presenter.isErrorMode &amp;&amp; !presenter.isShowAnswersActive &amp;&amp; !presenter.disabled &amp;&amp; presenter.toSelect &lt; (presenter.points).length &amp;&amp; presenter.isDown === true) {
                    presenter.drawTempLine(presenter.toSelect-1,presenter.mouseX,presenter.mouseY);
                } else {
                    if (presenter.$view.find('#line_tmp').length &gt; 0) {
                        presenter.$view.find('#line_tmp').remove();
                        presenter.isDown = false;
                    }
                }
            });

            presenter.$view.on('touchend', function(e){
                e.stopPropagation();
                e.preventDefault();
                tmpTime = presenter.time;
                if (presenter.$view.find('#line_tmp').length &gt; 0) {
                    presenter.$view.find('#line_tmp').remove();
                    tmpTime = 0;
                }

                var scaledPoint = scalePoint({x: presenter.mouseX, y: presenter.mouseY});
                presenter.mouseX = scaledPoint.x;
                presenter.mouseY = scaledPoint.y;

                distance = Math.abs(parseInt(presenter.points[presenter.toSelect][0],10) - presenter.mouseX) + Math.abs(parseInt(presenter.points[presenter.toSelect][1],10) - presenter.mouseY);
                if (distance &lt; 35) {
                    if (presenter.toSelect &gt; 0) {
                        presenter.$view.find('div#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(presenter.toSelect-1)).removeClass('active');
                        presenter.drawLine(presenter.toSelect,tmpTime,true);
                    }

                    if (presenter.toSelect &lt; (presenter.points).length-1) {
                        presenter.$view.find('div#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(presenter.toSelect)).addClass('active');
                    }

                    presenter.toSelect++;
                    presenter.draw++;
                    presenter.triggerPointSelectedEvent(i+1,1);
                }
                presenter.isDown = false;
            });
        }
        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        for (let i = 0; i &lt; events.length; i++) {
            presenter.eventBus.addEventListener(events[i], this);
        }
    };

    presenter.createPreview = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.$view = $(view);
        presenter.addonID = model.ID;
        presenter.model = model;
        var con = presenter.$view.find('.connectingdots').parent();
        presenter.$view.find('.connectingdots').css({
            'width': con.width(),
            'height': con.height()
        });
        presenter.imageSrcStart = presenter.model['Image A'];

        var coords = presenter.model['Dots'];
        presenter.points = getPoint(con,coords);
        presenter.indexes = checkIndexes(presenter.model['Indexes'],(presenter.points).length);

        var $div = presenter.$view.find('.connectingdots');
        var Width = $div.width();
        var Height = $div.height();

        if (presenter.points == false || presenter.indexes == false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
        } else {
            presenter.points = getPoint(con,coords);
            presenter.drawPoints();
            if (presenter.imageSrcStart != '') {
                var image1 = document.createElement('img');
                $(image1).attr('src', presenter.imageSrcStart);
                $(image1).addClass('image-start');
                presenter.$view.find('.connectingdots').prepend(image1);
            }

            var coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
                xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                coloringWrapper = presenter.$view.find('.connectingdots');

            coordinatesContainer.css({
                'width' : 35,
                'height' : 22,
                'border' : '1px solid #696969',
                'borderRadius' : '3px',
                'position' : 'absolute',
                'top' : 3,
                'left' : 3,
                'fontSize' : '9px',
                'padding' : '5px',
                'lineHeight' : '11px'
            });

            coordinatesContainer.append(xContainer).append(yContainer);

            coloringWrapper.append(coordinatesContainer);
            coloringWrapper.css({
                'position' : 'relative',
                'minHeight' : Height,
                'minWidth' : Width
            });

            function setCalculatedPosition(e) {
                presenter.mouseSX = parseInt(e.pageX,10) - parseInt($div.offset().left,10);
                presenter.mouseSY = parseInt(e.pageY,10) - parseInt($div.offset().top,10);
                xContainer.find('.value').html(presenter.mouseSX);
                yContainer.find('.value').html(presenter.mouseSY);
            }

            var doesElementExist = function() {
                var $moduleSelector = $('.moduleSelector[data-id="'+presenter.addonID+'"]');

                if ($moduleSelector.length &gt; 0) {
                    $moduleSelector.on('mousemove', function(e) {
                        setCalculatedPosition(e);
                    });

                    clearInterval(interval);
                }
            };

            var interval = setInterval(function() { doesElementExist(); }, 500);

            presenter.$view.find('.connectingdots').on('mousemove', function(e) {
                setCalculatedPosition(e);
            });
        }

    };

    presenter.upgradeModel = function(model) {
        return presenter.upgradeShowAllAnswersInGradualShowAnswersMode(model);
    };

    presenter.upgradeShowAllAnswersInGradualShowAnswersMode = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel["Show all answers in gradual show answers mode"]) {
            upgradedModel["Show all answers in gradual show answers mode"] = false;
        }

        return upgradedModel;
    };

    presenter.reset = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        if (presenter.error != false) {
        } else {
            if (presenter.toSelect &gt; 0) {
                var temp_id = '#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(presenter.toSelect-1);
                presenter.$view.find(temp_id).removeClass('active');
            }
            for (var i = 1; i &lt; presenter.toSelect; i++) {
                presenter.$view.find('#'+presenter.lineIds[i]).remove();
            }
            presenter.$view.find('.image-start').remove();
            presenter.$view.find('.image-end').remove();
            if (presenter.imageSrcStart != '') {
                var image1 = document.createElement('img');
                $(image1).attr('src', presenter.imageSrcStart);
                $(image1).addClass('image-start');
                presenter.$view.find('.connectingdots').prepend(image1);
            }
            presenter.isDisabled = presenter.initIsDisabled;
            presenter.isVisible = presenter.initIsVisible;
            presenter.toSelect = 0;
            presenter.updateVisibility();
            presenter.setWorkMode();
        }
    };

    function executeWithoutShownAnswers(funcToCall) {
        let wasShowAnswersActive = false;
        let wasGradualShowAnswersActive = false;
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
            wasShowAnswersActive = true;
        } else if (presenter.isGradualShowAnswersActive) {
            presenter.gradualHideAnswers();
            wasGradualShowAnswersActive = true;
        }

        const result = funcToCall();

        if (wasShowAnswersActive) {
            presenter.showAnswers();
        } else if (wasGradualShowAnswersActive) {
            presenter.gradualShowAnswers();
        }

        return result;
    }

    presenter.getState = function() {
        return executeWithoutShownAnswers(getState);
    };

    function getState() {
        return JSON.stringify({
            toSelect: presenter.toSelect,
            isDisabled: presenter.isDisabled,
            isVisible: presenter.isVisible
        });
    }

    presenter.setState = function(state) {
        presenter.toSelect = JSON.parse(state).toSelect;
        presenter.isVisible = JSON.parse(state).isVisible;
        presenter.isDisabled = JSON.parse(state).isDisabled;

        if (presenter.toSelect &gt; 0 &amp;&amp; presenter.toSelect &lt; (presenter.points).length) {
            var temp_id = '#dot_container_'+presenter.randomId+'_'+presenter.addonID+'_'+(presenter.toSelect-1);
            $(temp_id).addClass('active');
        }
        for(var i = 1; i &lt; presenter.toSelect; i++) {
            presenter.drawLine(i,0,false);
        }
        presenter.updateVisibility();
    };

    presenter.getMaxScore = function () {
        return presenter.activity ? 1 : 0;
    };

    presenter.getScore = function (view, model) {
        if (!presenter.activity || presenter.error != false) {
            return 0;
        }

        return (presenter.points).length === presenter.toSelect ? 1 : 0;
    };

    presenter.getErrorCount = function () {
        if (presenter.toSelect == 0 || (presenter.error != false)) {
            return 0;
        } else {
            return presenter.getMaxScore() - presenter.getScore();
        }
    };

    presenter.setShowErrorsMode = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isErrorMode = true;
        if (!presenter.activity) return 0;
        if (presenter.getScore() === presenter.getMaxScore())
            presenter.$view.find('.connectingdots').addClass('correct');
        if (presenter.getErrorCount() &gt; 0)
            presenter.$view.find('.connectingdots').addClass('wrong');
    };

    presenter.setWorkMode = function () {
        presenter.isErrorMode = false;
        presenter.$view.find('.connectingdots').removeClass('wrong');
        presenter.$view.find('.connectingdots').removeClass('correct');
    };

    presenter.showAnswers = function () {
        if (!presenter.activity) {
            return;
        }

        presenter.isErrorMode &amp;&amp; presenter.setWorkMode();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isShowAnswersActive = true;
        _showAnswers();
    };

    presenter.gradualShowAnswers = function (itemIndex) {
        if (!presenter.activity) {
            return;
        }

        presenter.isErrorMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();

        if (presenter.showAllAnswersInGradualShowAnswersMode) {
            _showAnswers();
        } else {
            const isFirstGSA = itemIndex === 0;
            if (isFirstGSA) {
                presenter.toSelect &gt; 0 &amp;&amp; getCurrentDotContainer().removeClass("active");
                for (let i = 1; i &lt; presenter.toSelect; i++) {
                    presenter.$view.find('#' + presenter.lineIds[i]).remove();
                }
            }
            presenter.drawLine(itemIndex + 1,0,true,true);
        }
    };

    function _showAnswers() {
        presenter.$view.find(".line").addClass("line-show-answer");

        let i = presenter.toSelect;
        if (i === 0) {
            i++;
        } else {
            getCurrentDotContainer().removeClass("active");
        }
        for (; i &lt; (presenter.points).length; i++) {
            presenter.drawLine(i,0,true,true);
        }
    }

    presenter.isShowingAnswers = function (){
        return presenter.isGradualShowAnswersActive || presenter.isShowAnswersActive;
    }

    presenter.hideAnswers = function () {
        if (!presenter.activity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.isShowAnswersActive = false;
        _hideAnswers();
    };

    presenter.gradualHideAnswers = function () {
        if (!presenter.activity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.isGradualShowAnswersActive = false;
        _hideAnswers();
        if (!presenter.showAllAnswersInGradualShowAnswersMode) {
            for (let i = 1; i &lt; presenter.toSelect; i++) {
                presenter.drawLine(i,0,true,false);
            }
        }
    };

    function _hideAnswers() {
        presenter.$view.find(".line").removeClass("line-show-answer");
        presenter.$view.find(".line-show-answer").remove();
        if (presenter.toSelect &gt; 0 &amp;&amp; presenter.toSelect &lt; (presenter.points).length) {
            getCurrentDotContainer().addClass("active");
        }
    }

    presenter.getActivitiesCount = function () {
        if (presenter.error) {
            return 0;
        }
        if (presenter.showAllAnswersInGradualShowAnswersMode) {
            return 1;
        }

        return (presenter.points).length - 1;
    };

    function getCurrentDotContainer() {
        return getDotContainer(presenter.toSelect - 1);
    }

    function getDotContainer(dotIndex) {
        return presenter.$view.find("div#dot_container_" + presenter.randomId + '_' + presenter.addonID + '_' + dotIndex);
    }

    function scalePoint({x, y}) {
        var scaledPoint = {x: x, y: y};
        if (!presenter.playerController)
            return scaledPoint;

        var scale = presenter.playerController.getScaleInformation();
        if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
            scaledPoint.x = Math.floor(scaledPoint.x / scale.scaleX);
            scaledPoint.y = Math.floor(scaledPoint.y / scale.scaleY);
        }
        return scaledPoint;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Connection" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Is not an activity" name="isNotActivity" nameLabel="Connection_property_is_not_activity" type="boolean"/>
        <property name="Single connection mode" nameLabel="Connection_property_single_connection_mode" type="boolean"/>
        <property isDefault="true" name="Left column" nameLabel="Connection_property_left_column" type="list">
            <property name="id" nameLabel="Connection_property_id" type="string"/>
            <property isLocalized="true" name="content" nameLabel="Connection_property_content" type="html"/>
            <property name="connects to" nameLabel="Connection_property_connects_to" type="string"/>
            <property name="additional class" nameLabel="Connection_property_additional_class" type="string"/>
        </property>
        <property name="Right column" nameLabel="Connection_property_right_column" type="list">
            <property name="id" nameLabel="Connection_property_id" type="string"/>
            <property isLocalized="true" name="content" nameLabel="Connection_property_content" type="html"/>
            <property name="connects to" nameLabel="Connection_property_connects_to" type="string"/>
            <property name="additional class" nameLabel="Connection_property_additional_class" type="string"/>
        </property>
        <property name="Columns width" nameLabel="Connection_property_columns_width" type="list">
            <property name="left" nameLabel="Connection_property_left" type="string"/>
            <property name="middle" nameLabel="Connection_property_middle" type="string"/>
            <property name="right" nameLabel="Connection_property_right" type="string"/>
        </property>
        <property name="initialConnections" nameLabel="Connection_property_initial_connection" type="list">
            <property name="from" nameLabel="Connection_property_initial_connection_from" type="string"/>
            <property name="to" nameLabel="Connection_property_initial_connection_to" type="string"/>
            <property name="isDisabled" nameLabel="Connection_property_initial_connection_is_disabled" type="boolean"/>
        </property>
        <property name="Default connection color" nameLabel="Connection_property_default_connection_color" type="string"/>
        <property name="Correct connection color" nameLabel="Connection_property_correct_connection_color" type="string"/>
        <property name="disabledConnectionColor" nameLabel="Connection_property_disabled_connection_color" type="string"/>
        <property name="Incorrect connection color" nameLabel="Connection_property_incorrect_connection_color" type="string"/>
        <property name="Connection thickness" nameLabel="Connection_property_connection_thickness" type="string"/>
        <property name="Random order left column" nameLabel="Connection_property_random_order_left_column" type="boolean"/>
        <property name="Random order right column" nameLabel="Connection_property_random_order_right_column" type="boolean"/>
        <property name="Show answers line color" nameLabel="Connection_property_show_answers_line_color" type="string"/>
        <property displayName="Block wrong answers" name="blockWrongAnswers" nameLabel="Connection_property_block_wrong_answers" type="boolean"/>
        <property displayName="Remove dragged element" name="removeDraggedElement" nameLabel="Connection_property_remove_dragged_element" type="boolean"/>

        <property displayName="Lang attribute" name="langAttribute" nameLabel="Connection_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Connection_property_speech_texts" type="staticlist">

            <property name="Connected" nameLabel="Connection_connected" type="staticrow">
                <property name="Connected" nameLabel="Connection_connected" type="string"/>
            </property>

            <property name="Disconnected" nameLabel="Connection_disconnected" type="staticrow">
                <property name="Disconnected" nameLabel="Connection_disconnected" type="string"/>
            </property>

            <property name="ConnectedTo" nameLabel="Connection_connected_to" type="staticrow">
                <property name="Connected to" nameLabel="Connection_connected_to" type="string"/>
            </property>

            <property name="Selected" nameLabel="Connection_selected" type="staticrow">
                <property name="Selected" nameLabel="Connection_selected" type="string"/>
            </property>

            <property name="Deselected" nameLabel="Connection_deselected" type="staticrow">
                <property name="Deselected" nameLabel="Connection_deselected" type="string"/>
            </property>

            <property name="Correct" nameLabel="Connection_correct" type="staticrow">
                <property name="Correct" nameLabel="Connection_correct" type="string"/>
            </property>

            <property name="Wrong" nameLabel="Connection_wrong" type="staticrow">
                <property name="Wrong" nameLabel="Connection_wrong" type="string"/>
            </property>
        </property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property name="Orientations" nameLabel="Connection_orientations_list" type="list">
            <property name="Layout" nameLabel="Connection_orientation_layout" type="string"/>
            <property name="Orientation" nameLabel="Connection_orientation_name" type="{Vertical, Horizontal}"/>
        </property>
    </model>
<css>.connectionContainer {
    height: 100%;
    width: 100%;
    padding: 0px;
    border-collapse:collapse;
    border-spacing: 0px;
    direction: ltr;
}

.connectionContainer .connectionLeftColumn {
    width: 33%;
    height: 100%;
    padding: 0px;
}

.connectionContainer .connectionMiddleColumn {
    width: 34%;
    height: 100%;
    padding: 0px;
    overflow: hidden;
    overflow-x: visible;
}

.connectionContainer .connectionRightColumn {
    width: 33%;
    height: 100%;
    padding: 0px;
}

.connectionContainer .connectionTopRow {
    width: 100%;
    height: 33%;
    padding: 0px;
}

.connectionContainer .connectionMiddleRow {
    width: 100%;
    height: 33%;
    padding: 0px;
    overflow: hidden;
    overflow-y: visible;
}

.connectionContainer .connectionBottomRow {
    width: 100%;
    height: 33%;
    padding: 0px;
}

.connectionContainer .connections {
    width: 100%;
    height: 100%;
}

.connectionContainer .connectionItem {
    width: 100%;
    padding: 0px;
    border-collapse:collapse;
    border-spacing: 0px;
    border: solid 1px black;
}

.connectionContainer .connectionMiddleRow &gt; td {
    display: flex;
}

.connectionContainer .answerItem {
    width: 100%;
    padding: 0px;
}

.connectionContainer .content {
    height: 100%;
    width: 100%;
    border-collapse:collapse;
    border-spacing: 0px;
}

.connectionContainer .content .connectionItemWrapper {
    padding: 0px;
}

.connectionContainer .connectionTopRow .content .connectionItemWrapper {
    vertical-align: bottom;
}

.connectionContainer .connectionBottomRow .content .connectionItemWrapper {
    vertical-align: top;
}

.connection_line {
    position: absolute;
    height:2px;
}

.correctAnswerDiv::before {
    content: "\2714";
}

.inCorrectAnswerDiv::before {
    content: "\2716";
}

.printable_addon_Connection .connectionContainer .connections {
    width: 100%;
    height: 100%;
    vertical-align: middle;
}
</css><view>&lt;table class="connectionContainer"&gt;&lt;/table&gt;
</view><preview>&lt;table class="connectionContainer"&gt;&lt;/table&gt;
</preview><presenter>function AddonConnection_create() {

    function getTextVoiceObject(text, lang) {
        return {text: text, lang: lang};
    }

    var presenter = function () {
    };

    var playerController;
    var eventBus;
    var addonID;
    var isWCAGOn = false;
    var currentLayoutName = "";

    presenter.uniqueIDs = [];
    presenter.elements = [];
    presenter.lastClickTime = 0;
    presenter.lastEvent = null;
    presenter.disabledConnections = [];
    presenter.keyboardControllerObject = null;
    presenter.speechTexts = {};
    presenter.columnSizes = {};
    presenter.lineStackSA = [];

    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.GSAcounter = 0;
    presenter.isCheckActive = false;
    presenter.initialState = null;
    presenter.isHorizontal = false;

    presenter.mathJaxLoaders = {
        runLoaded: false,
        setStateLoaded: true,
    };

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

    presenter.printableState = null;
    presenter.printableParserID = "";
    presenter.printableParserCallback = null;
    presenter.printableClassNames = [];
    presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;

    presenter.CSS_CLASSES = {
        CONNECTION_CONTAINER: "connectionContainer",
        CONNECTIONS_LEFT_COLUMN: "connectionLeftColumn",
        CONNECTIONS_MIDDLE_COLUMN: "connectionMiddleColumn",
        CONNECTIONS_RIGHT_COLUMN: "connectionRightColumn",
        CONNECTIONS_TOP_ROW: "connectionTopRow",
        CONNECTIONS_MIDDLE_ROW: "connectionMiddleRow",
        CONNECTIONS_BOTTOM_ROW: "connectionBottomRow",
        CONTENT: "content",
        CONNECTIONS: "connections",
        CONNECTION_ITEM: "connectionItem",
        CONNECTION_ITEM_WRAPPER: "connectionItemWrapper",
        ICON: "icon",
        ICON_WRAPPER: "iconWrapper",
        INNER: "inner",
        INNER_WRAPPER: "innerWrapper",
        SELECTED: "selected",
    };

    var deferredCommandQueue = window.DecoratorUtils.DeferredSyncQueue(checkIsMathJaxLoaded);

    function checkIsMathJaxLoaded() {
        return presenter.mathJaxLoaders.runLoaded
            &amp;&amp; presenter.mathJaxLoaders.setStateLoaded
    }

    var connections;
    var selectedItem = null;

    presenter.lineStack = new LineStack(true);
    presenter.correctConnections = new LineStack(false);
    var isSelectionPossible = true;
    var isRTL = false;


    var CORRECT_ITEM_CLASS = 'connectionItem-correct';
    var WRONG_ITEM_CLASS = 'connectionItem-wrong';

    presenter.ERROR_MESSAGES = {
        'ID not unique': 'One or more IDs are not unique.',
        'One or two not exist': 'Provided id for initial value doesn\'t exists',
        'Are from the same column': 'Provided ids for initial value are in the same column',
        'Are from the same row': 'Provided ids for initial value are in the same row',
        'Orientation layout duplication': 'More than one orientation has been assigned for at least one layout.'
    };

    presenter.ELEMENT_SIDE = {
        LEFT: 0,
        RIGHT: 1
    };

    function isEnabledOrMultiLineMode(element) {
        if (!presenter.configuration.isSingleConnectionMode) {
            return true;
        }

        var elementId = convertId($(element).attr('id'));

        for (var i = 0; i &lt; presenter.configuration.initialConnections.length; i++) {
            var initialValue = presenter.configuration.initialConnections[i];

            if (initialValue.from === elementId || initialValue.to === elementId) {
                return !initialValue.isDisabled;
            }
        }

        return true;
    }

    function convertId(id) {
        id = id.toString();

        return id.substr(id.indexOf('-') + 1);
    }

    function convertIds(id1, id2) {
        return {
            id1: convertId(id1),
            id2: convertId(id2)
        };
    }

    function isOneOfValuesEmpty(initialValue) {
        return initialValue.from.trim() === "" &amp;&amp; initialValue.to.trim() === "";
    }

    presenter.getCurrentActivatedElement = function () {
        return $('.keyboard_navigation_active_element');
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeFrom_01(model);
        upgradedModel = presenter.upgradeStartValues(upgradedModel);
        upgradedModel = presenter.upgradeOrientations(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Connected"]) {
            upgradedModel["speechTexts"]["Connected"] = {Connected: ""};
        }
        if (!upgradedModel["speechTexts"]["Disconnected"]) {
            upgradedModel["speechTexts"]["Disconnected"] = {Disconnected: ""};
        }
        if (!upgradedModel["speechTexts"]["Selected"]) {
            upgradedModel["speechTexts"]["Selected"] = {Selected: ""};
        }
        if (!upgradedModel["speechTexts"]["Deselected"]) {
            upgradedModel["speechTexts"]["Deselected"] = {Deselected: ""};
        }
        if (!upgradedModel["speechTexts"]["Correct"]) {
            upgradedModel["speechTexts"]["Correct"] = {Correct: ""};
        }
        if (!upgradedModel["speechTexts"]["Wrong"]) {
            upgradedModel["speechTexts"]["Wrong"] = {Wrong: ""};
        }
        if (!upgradedModel["speechTexts"]["ConnectedTo"]) {
            upgradedModel["speechTexts"]["ConnectedTo"] = {"Connected to": ""};
        }

        return upgradedModel;
    };

    presenter.upgradeStartValues = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['initialConnections'] === undefined) {
            upgradedModel['initialConnections'] = [];
            upgradedModel['disabledConnectionColor'] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["Columns width"]) {
            upgradedModel["Columns width"] = [
                {
                    left: "",
                    middle: "",
                    right: ""
                }
            ];
        }

        return upgradedModel;
    };

    presenter.upgradeOrientations = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["Orientations"] === undefined) {
            upgradedModel["Orientations"] = [];
        }

        return upgradedModel;
    };

    function LineStack(sendEvents) {
        this.stack = [];
        this.ids = [];
        this.sendEvents = sendEvents;
        this.shouldFireEvent = true;

        this.setSendEvents = function (value) {
            this.sendEvents = value;
        };

        this.push = function (line) {
            var pair = [], score, i;
            for (i = 0; i &lt; presenter.elements.length; i++) {
                if (presenter.elements[i].element.get(0) == line.from.get(0) || presenter.elements[i].element.get(0) == line.to.get(0)) {
                    pair.push(presenter.elements[i].id);
                    if (pair.length == 2) {
                        break;
                    }
                }
            }
            this.ids.push(pair);
            this.stack.push(line);

            if (this.sendEvents) {
                score = presenter.correctConnections.hasLine(line).length &gt; 0 ? 1 : 0;
                presenter.sendEvent(pair[0], pair[1], 1, score);
                if (score == 0 &amp;&amp; presenter.configuration.blockWrongAnswers) {
                    this.shouldFireEvent = false;
                    this.remove(line);
                }
            }
        };

        this.length = function () {
            return this.stack.length;
        };

        this.get = function (i) {
            return this.stack[i];
        };

        this.remove = function (line) {
            function getID(element) {
                // innerWrapper -&gt; td -&gt; tr -&gt; tbody -&gt; table (which has id attribute)
                var rawID = $(element).find('.innerWrapper').parent().parent().parent().parent().attr('id');
                return rawID.split('connection-')[1];
            }

            var linePosition = this.stack.indexOf(line);
            this.stack.splice(linePosition, 1);
            var lineFromID = getID(line.from);
            var lineToID = getID(line.to);
            var pair = [lineFromID, lineToID];
            var score;

            for (var i = 0; i &lt; this.ids.length; i++) {
                if ((this.ids[i][0] == pair[0] &amp;&amp; this.ids[i][1] == pair[1]) ||
                    (this.ids[i][0] == pair[1] &amp;&amp; this.ids[i][1] == pair[0])) {
                    this.ids.splice(i, 1);
                    break;
                }
            }

            if (this.sendEvents &amp;&amp; this.shouldFireEvent) {
                score = presenter.correctConnections.hasLine(line).length &gt; 0 ? 1 : 0;
                presenter.sendEvent(pair[0], pair[1], 0, score);
            }
            this.shouldFireEvent = true;
        };

        this.clear = function () {
            this.ids.splice(0, this.ids.length);
            this.stack.splice(0, this.stack.length);
        };

        this.hasLine = function (line) {
            for (var i = 0, stackLength = this.stack.length; i &lt; stackLength; i++) {
                if ((this.stack[i].from.get(0) == line.from.get(0) &amp;&amp; this.stack[i].to.get(0) == line.to.get(0)) ||
                    (this.stack[i].from.get(0) == line.to.get(0) &amp;&amp; this.stack[i].to.get(0) == line.from.get(0))) {
                    return [this.stack[i]];
                }
            }
            return [];
        };

        this.hasPair = function (pair) {
            for (var i = 0; i &lt; this.ids.length; i++) {
                if (this.ids[i][0] == pair[0] &amp;&amp; this.ids[i][1] == pair[1] ||
                    this.ids[i][1] == pair[0] &amp;&amp; this.ids[i][0] == pair[1]) {
                    return true;
                }
            }
            return false;
        };

        this.isItemUsed = function (line) {
            var lines = [];
            for (var i = 0; i &lt; this.stack.length; i++) {
                if ((this.stack[i].from.get(0) == line.from.get(0) || this.stack[i].to.get(0) == line.to.get(0)) ||
                    (this.stack[i].from.get(0) == line.to.get(0) || this.stack[i].to.get(0) == line.from.get(0))) {
                    lines.push(this.stack[i])
                }
            }
            return lines;
        };

        this.getDisabledCount = function () {
            return this.stack.filter(function (stackElement) {
                return stackElement.isDisabled();
            }).length;
        };
    }

    function Line(from, to) {
        this.from = from;
        this.to = to;

        this.connects = function (element) {
            return from.get(0) == element.get(0) || to.get(0) == element.get(0);
        };

        this.otherSide = function (element) {
            if (from.get(0) == element.get(0)) {
                return to;
            }
            return from;
        };

        this.isDisabled = function () {
            var ids = convertIds($(this.from).attr('id'), $(this.to).attr('id'));

            for (var i = 0; i &lt; presenter.configuration.initialConnections.length; i++) {
                var initialValue = presenter.configuration.initialConnections[i];

                var initialValues = [initialValue.from, initialValue.to];

                if (initialValue.isDisabled) {
                    if ($.inArray(ids.id1, initialValues) &gt; -1 &amp;&amp; $.inArray(ids.id2, initialValues) &gt; -1) {
                        return true;
                    }
                }
            }

            return false;
        };
    }

    presenter.showErrorMessage = function (view, errorCode) {
        return $(view).html(presenter.ERROR_MESSAGES[errorCode]);
    };

    presenter.parseDefinitionLinks = function () {
        presenter.textParser.connectLinks(presenter.$view);
    };

    presenter.parseAddonGaps = function (model) {
        //This method is only needed in preview, in normal mode text parser will handle addon gaps
        for (var i = 0; i &lt; model["Left column"].length; i++) {
            var el = model["Left column"][i];
            el.content = parseAddonGapsInContent(el.content);
        }
        for (var i = 0; i &lt; model["Right column"].length; i++) {
            var el = model["Right column"][i];
            el.content = parseAddonGapsInContent(el.content);
        }
        return model;
    };

    function parseAddonGapsInContent (text) {
        return text.replace(/\\addon{([^\s-]*?)}/g, '&lt;div id="addonGap-$1"&gt;$1&lt;/div&gt;');
    }

    presenter.removeNonVisibleInnerHTML = function () {
        presenter.removeNonVisibleInnerHTMLForRoot(presenter.$view);

    };

    presenter.removeNonVisibleInnerHTMLForRoot = function ($root) {
        $.each($root.find('.' + presenter.CSS_CLASSES.INNER_WRAPPER), function (index, element) {
            var newInnerHtml = $(element).html().replace(/\\alt{([^{}|]*?)\|[^{}|]*?}(\[[a-zA-Z0-9_\- ]*?\])*/g, '$1'); // replace \alt{a|b}[c] with a
            $(element).html(newInnerHtml.replace(/\\alt{([^|{}]*?)\|[^|{}]*?}/g, '$1')); // replace \alt{a|b} with a
        });
    };

    presenter.setPlayerController = function (controller) {
        presenter.registerMathJax();

        playerController = controller;

        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        presenter.textParser = new TextParserProxy(getTextParser());
    };

    presenter.registerMathJax = function AddonConnection_registerMathJax() {
        var mathJaxDeferred = new jQuery.Deferred();
        presenter.mathJaxProcessEndedDeferred = mathJaxDeferred;
        presenter.mathJaxProcessEnded = mathJaxDeferred.promise();

        MathJax.Hub.Register.MessageHook("End Process", function (message) {
            if ($(message[1]).hasClass('ic_page')) {
                presenter.mathJaxProcessEndedDeferred.resolve();
            }
            if ($(message[1]).hasClass('ic_popup_page')) {
                presenter.mathJaxProcessEndedDeferred.resolve();
            }
        });
    };

    presenter.setLengthOfSideObjects = function (view) {
        const $firstSide = $(view).find("." + presenter.getFirstSideCSSClassName() + ":first");
        const $middleSide = $(view).find("." + presenter.getMiddleSideCSSClassName() + ":first");
        const $secondSide = $(view).find("." + presenter.getSecondSideCSSClassName() + ":first");

        let firstSideLength, middleSideLength, secondSideLength;
        if (presenter.isHorizontal) {
            firstSideLength = "auto";
            middleSideLength = "auto";
            secondSideLength = "auto";
        } else {
            firstSideLength = presenter.configuration.columnsWidth.left;
            middleSideLength = presenter.configuration.columnsWidth.middle;
            secondSideLength = presenter.configuration.columnsWidth.right;

            if (!firstSideLength) firstSideLength = "auto";
            if (!middleSideLength) middleSideLength = "auto";
            if (!secondSideLength) secondSideLength = "auto";
        }

        const propertyName = presenter.isHorizontal ? "height" : "width";
        $firstSide.css(propertyName, firstSideLength);
        $middleSide.css(propertyName, middleSideLength);
        $secondSide.css(propertyName, secondSideLength);
    };

    presenter.run = function (view, model) {
        addonID = model.ID;

        presenter.initialize(view, model, false);

        presenter.parseDefinitionLinks();
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;

        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            eventBus.addEventListener(events[i], this);
        }
    };

    presenter.createPreview = function (view, model) {
        model = presenter.parseAddonGaps(model);
        presenter.lineStack = new LineStack(false);
        presenter.initialize(view, model, true);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    function setSpeechTexts(speechTexts) {
        presenter.speechTexts = {
            connected: 'connected',
            disconnected: 'disconnected',
            connectedTo: 'connected to',
            selected: 'selected',
            deselected: 'deselected',
            correct: 'correct',
            wrong: 'wrong'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            connected: TTSUtils.getSpeechTextProperty(speechTexts['Connected']['Connected'], presenter.speechTexts.connected),
            disconnected: TTSUtils.getSpeechTextProperty(speechTexts['Disconnected']['Disconnected'], presenter.speechTexts.disconnected),
            connectedTo: TTSUtils.getSpeechTextProperty(speechTexts['ConnectedTo']['Connected to'], presenter.speechTexts.connectedTo),
            selected: TTSUtils.getSpeechTextProperty(speechTexts['Selected']['Selected'], presenter.speechTexts.selected),
            deselected: TTSUtils.getSpeechTextProperty(speechTexts['Deselected']['Deselected'], presenter.speechTexts.deselected),
            correct: TTSUtils.getSpeechTextProperty(speechTexts['Correct']['Correct'], presenter.speechTexts.correct),
            wrong: TTSUtils.getSpeechTextProperty(speechTexts['Wrong']['Wrong'], presenter.speechTexts.wrong)
        };
    }

    presenter.addDisabledElementsFromInitialValues = function () {
        presenter.configuration.initialConnections.forEach(function (initialValue) {
            if (initialValue.isDisabled) {
                presenter.disabledConnections.push({
                    id1: initialValue.from,
                    id2: initialValue.to
                });
            }
        });
    };


    presenter.getInitialValues = function (model) {
        var modelValidator = new ModelValidator();
        var validated = modelValidator.validate(model, [
            ModelValidators.List("initialConnections", [
                ModelValidators.String("from", {default: "", trim: false}),
                ModelValidators.String("to", {default: "", trim: false}),
                ModelValidators.Boolean("isDisabled", {default: false})
            ]),
            ModelValidators.String("disabledConnectionColor", {default: "", trim: true})
        ]);

        return {
            initialConnections: validated.value.initialConnections,
            disabledConnectionColor: validated.value.disabledConnectionColor
        };
    };

    presenter.validateInitialConnection = function (initialValue) {
        function containsID(array, idToFind, toReturn) {
            for (var i = 0; i &lt; array.length; i++) {
                if (array[i].id === idToFind) {
                    return toReturn;
                }
            }

            return false;
        }

        function areFromDifferentCols() {
            var fromColumn = containsID(presenter.model['Left column'], initialValue.from, "LEFT") ||
                containsID(presenter.model['Right column'], initialValue.from, "RIGHT");

            var toColumn = containsID(presenter.model['Left column'], initialValue.to, "LEFT") ||
                containsID(presenter.model['Right column'], initialValue.to, "RIGHT");

            return fromColumn !== toColumn;
        }

        function bothExists() {
            var fromExists = containsID(presenter.model['Left column'], initialValue.from, true) ||
                containsID(presenter.model['Right column'], initialValue.from, true);

            var toExists = containsID(presenter.model['Left column'], initialValue.to, true) ||
                containsID(presenter.model['Right column'], initialValue.to, true);

            return fromExists &amp;&amp; toExists;
        }

        if (!isOneOfValuesEmpty(initialValue)) {
            if (!bothExists()) {
                return getErrorObject("One or two not exist");
            } else if (!areFromDifferentCols()) {
                const errorCode = presenter.isHorizontal ? "Are from the same row" : "Are from the same column";
                return getErrorObject(errorCode);
            }
        }

        return getValidObject(initialValue);
    };

    presenter.validateInitialConnections = function (initialConnections) {
        for (let validatedInitialValue, i = 0; i &lt; initialConnections.length; i++) {
            validatedInitialValue = presenter.validateInitialConnection(initialConnections[i]);
            if (!validatedInitialValue.isValid) {
                return validatedInitialValue;
            }
        }

        return getValidObject(initialConnections);
    };

    /**
     * @param initialValue {{from: string, to: string}}
     */
    presenter.drawInitialValue = function (initialValue) {
        if (!isOneOfValuesEmpty(initialValue)) {
            pushConnection(new Line(getElementById(initialValue.from), getElementById(initialValue.to)), false);
        }
    };

    presenter.drawInitialValues = function () {
        this.lineStack.setSendEvents(false);

        if (!presenter.lineStack.length()) {
            presenter.configuration.initialConnections.forEach(presenter.drawInitialValue);
        }

        presenter.redraw();

        this.lineStack.setSendEvents(true);
    };

    presenter.initialize = function (view, model, isPreview) {
        model = presenter.upgradeModel(model);
        presenter.model = model;
        presenter.view = view;
        presenter.$view = $(view);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_MESSAGES, presenter.configuration.errorCode);
            return false;
        }

        presenter.chooseOrientation(model, true);
        presenter.setUpViewBody(presenter.view);
        connections = presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTIONS + ':first');

        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.setVisibility(presenter.isVisible || isPreview);
        isRTL = presenter.$view.css('direction').toLowerCase() === 'rtl';

        presenter.loadElements(view, model);

        const $connectionContainer = $("." + presenter.CSS_CLASSES.CONNECTION_CONTAINER);
        if (isPreview) {
            waitForLoad($connectionContainer, () =&gt; {
                presenter.initializeView(view, model);
                presenter.drawConfiguredConnections();
            })
            presenter.removeNonVisibleInnerHTML();
        } else {
            presenter.mathJaxProcessEnded.then(function () {
                waitForLoad($connectionContainer, () =&gt; {
                    presenter.initializeView(view, model);
                    presenter.drawInitialValues();
                    presenter.addDisabledElementsFromInitialValues();
                    presenter.mathJaxLoaders.runLoaded = true;
                    deferredCommandQueue.resolve();
                })
                presenter.registerListeners(presenter.view);
            });
        }

        setSpeechTexts(model["speechTexts"]);
        presenter.gatherCorrectConnections();
        presenter.buildKeyboardController();
    };

    presenter.validateModel = function (model) {
        const validatedOrientations = presenter.validateOrientations(model["Orientations"]);
        if (!validatedOrientations.isValid) {
            return validatedOrientations;
        }

        const initialValues = presenter.getInitialValues(model);
        const validatedInitialConnections = presenter.validateInitialConnections(initialValues.initialConnections)
        if (!validatedInitialConnections.isValid) {
            return validatedInitialConnections;
        }

        return {
            isValid: true,
            addonID: model["ID"],
            langTag: model["langAttribute"],
            isNotActivity: ModelValidationUtils.validateBoolean(model["isNotActivity"] || "False"),
            blockWrongAnswers: ModelValidationUtils.validateBoolean(model.blockWrongAnswers),
            removeDraggedElement: ModelValidationUtils.validateBoolean(model.removeDraggedElement),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]),
            connectionThickness: getModelValue(model, "Connection thickness", "1px"),
            defaultConnectionColor: getModelValue(model, "Default connection color", "#000"),
            correctConnectionColor: getModelValue(model, "Correct connection color", "#0d0"),
            incorrectConnectionColor: getModelValue(model, "Incorrect connection color", "#d00"),
            showAnswersLineColor: getModelValue(model, "Show answers line color", "#0d0"),
            isSingleConnectionMode: model["Single connection mode"].toLowerCase() === "true",
            orientations: validatedOrientations.value,
            initialConnections: initialValues.initialConnections,
            disabledConnectionColor: initialValues.disabledConnectionColor,
            columnsWidth: model["Columns width"][0],
        };
    };

    function getModelValue(model, modelKey, defaultValue) {
        return model[modelKey] != '' ? model[modelKey] : defaultValue;
    }

    presenter.validateOrientations = function (orientations) {
        const usedLayoutNames = [];
        for (let orientationConfig, i = 0; i &lt; orientations.length; i++) {
            orientationConfig = orientations[i];
            if (usedLayoutNames.includes(orientationConfig.Layout)) {
                return getErrorObject("Orientation layout duplication");
            }
            usedLayoutNames.push(orientationConfig.Layout);
        }
        return getValidObject(orientations);
    }

    function getValidObject(value) {
        return {
            isValid: true,
            value: value
        }
    }

    function getErrorObject(errorCode, itemIndex = undefined) {
        return {
            isValid: false,
            errorCode: errorCode,
            itemIndex: itemIndex
        }
    }

    presenter.setCurrentLayoutName = function(layoutName) {
        currentLayoutName = layoutName;
    }

    presenter.chooseOrientation = function (model, supportsVerticalOrientation) {
        presenter.isHorizontal = false;
        if (!supportsVerticalOrientation) {
            return;
        }

        for (let orientationConfig, i = 0; i &lt; model["Orientations"].length; i++) {
            orientationConfig = model["Orientations"][i];
            if (orientationConfig.Layout === currentLayoutName) {
                presenter.isHorizontal = orientationConfig.Orientation === "Horizontal";
                return;
            }
        }
    }

    /**
     * Expand the view with a body consisting of three empty objects (objects without elements):
     * 1. Object in which the elements from the first side will be placed (left column/top row).
     * 2. Object for creating connections (middle column/middle row).
     * 3. Object in which the elements from the second side will be placed (right column/bottom row).
     * If isHorizontal then sides are created as rows, otherwise as columns.
     * @return undefined
    **/
    presenter.setUpViewBody = function (view) {
        $(view).attr("lang", presenter.configuration.langTag);

        const elementToExpand = view.getElementsByClassName(presenter.CSS_CLASSES.CONNECTION_CONTAINER)[0];
        const viewBody = presenter.isHorizontal ? createEmptyHorizontalViewBody() : createEmptyVerticalViewBody();
        elementToExpand.append(viewBody);

        presenter.setLengthOfSideObjects(view);
    }

    function createEmptyHorizontalViewBody() {
        const viewBody = document.createElement("tbody");

        appendHorizontalViewBodyTopRow(viewBody);
        appendHorizontalViewBodyMiddleRow(viewBody);
        appendHorizontalViewBodyBottomRow(viewBody);

        return viewBody;
    }

    function appendHorizontalViewBodyTopRow(view) {
        const cellContent = createCellContent();
        const row = createHorizontalViewBodyRow(presenter.CSS_CLASSES.CONNECTIONS_TOP_ROW, cellContent);
        view.append(row);
    }

    function appendHorizontalViewBodyMiddleRow(view) {
        const cellContent = createCellCanvas();
        const row = createHorizontalViewBodyRow(presenter.CSS_CLASSES.CONNECTIONS_MIDDLE_ROW, cellContent);
        view.append(row);
    }

    function appendHorizontalViewBodyBottomRow(view) {
        const cellContent = createCellContent();
        const row = createHorizontalViewBodyRow(presenter.CSS_CLASSES.CONNECTIONS_BOTTOM_ROW, cellContent);
        view.append(row);
    }

    function createHorizontalViewBodyRow(className, content) {
        const row = document.createElement("tr");
        row.classList.add(className);

        const cell = document.createElement("td");
        cell.append(content);
        row.append(cell);
        return row;
    }

    function createEmptyVerticalViewBody() {
        const viewBody = document.createElement("tbody");

        const sharedRow = document.createElement("tr");
        viewBody.append(sharedRow);

        appendHorizontalViewBodyLeftColumn(sharedRow);
        appendHorizontalViewBodyMiddleColumn(sharedRow);
        appendHorizontalViewBodyBottomColumn(sharedRow);

        return viewBody;
    }

    function appendHorizontalViewBodyLeftColumn(view) {
        const cellContent = createCellContent();
        const column = createVerticalViewBodyColumn(presenter.CSS_CLASSES.CONNECTIONS_LEFT_COLUMN, cellContent);
        view.append(column);
    }

    function appendHorizontalViewBodyMiddleColumn(view) {
        const cellContent = createCellCanvas();
        const column = createVerticalViewBodyColumn(presenter.CSS_CLASSES.CONNECTIONS_MIDDLE_COLUMN, cellContent);
        view.append(column);
    }

    function appendHorizontalViewBodyBottomColumn(view) {
        const cellContent = createCellContent();
        const column = createVerticalViewBodyColumn(presenter.CSS_CLASSES.CONNECTIONS_RIGHT_COLUMN, cellContent);
        view.append(column);
    }

    function createVerticalViewBodyColumn(className, content) {
        const cell = document.createElement("td");
        cell.classList.add(className);
        cell.append(content);
        return cell;
    }

    function createCellCanvas() {
        const canvas = document.createElement("canvas");
        canvas.classList.add(presenter.CSS_CLASSES.CONNECTIONS);
        return canvas;
    }

    function createCellContent() {
        const content = document.createElement("table");
        content.classList.add(presenter.CSS_CLASSES.CONTENT);

        const contentBody = document.createElement("tbody");
        content.append(contentBody);

        return content;
    }

    presenter.buildKeyboardController = function () {
        var elements = [];
        for (var i = 0; i &lt; presenter.elements.length; i++) {
            elements.push($(presenter.elements[i].element));
        }

        presenter.keyboardControllerObject = new ConnectionKeyboardController(elements, 2);
        presenter.keyboardControllerObject.selectEnabled(true);
    };

    function getElementById(id) {
        for (var i = 0; i &lt; presenter.elements.length; i++) {
            if (presenter.elements[i].id == id) {
                return presenter.elements[i].element;
            }
        }

        return -1;
    }

    presenter.establishElementSide = function (elementID, modelLeftSide, modelRightSide) {
        var i, arrayLength;

        for (i = 0, arrayLength = modelLeftSide.length; i &lt; arrayLength; i += 1) {
            if (modelLeftSide[i].id === elementID) {
                return presenter.ELEMENT_SIDE.LEFT;
            }
        }

        for (i = 0, arrayLength = modelRightSide.length; i &lt; arrayLength; i += 1) {
            if (modelRightSide[i].id === elementID) {
                return presenter.ELEMENT_SIDE.RIGHT;
            }
        }
    };

    presenter.createEventData = function (source, elementFromID, elementToID, model, value, score) {
        var leftColumn = model["Left column"];
        var rightColumn = model["Right column"];

        var fromElementSide = presenter.establishElementSide(elementFromID, leftColumn, rightColumn);
        var itemStr = "";
        switch (fromElementSide) {
            case presenter.ELEMENT_SIDE.LEFT:
                itemStr = elementFromID + '-' + elementToID;
                break;
            case presenter.ELEMENT_SIDE.RIGHT:
                itemStr = elementToID + '-' + elementFromID;
                break;
        }

        return {
            'source': source,
            'item': itemStr,
            'value': value.toString(),
            'score': score.toString()
        };
    };

    presenter.sendEvent = function (fromID, toID, value, score) {
        if (!presenter.isShowingAnswers()) {
            var eventData = presenter.createEventData(addonID, fromID, toID, presenter.model, value, score);
            eventBus.sendEvent('ValueChanged', eventData);
            if (presenter.isAllOK()) {
                sendAllOKEvent();
            }
        }
    };

    function sendAllOKEvent() {
        var eventData = {
            'source': addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    }

    function clickLogic(element) {
        if (basicClickLogic(element)) {
            presenter.redraw();
        }
    }

    presenter.getFirstSideCSSClassName = function () {
        return presenter.isHorizontal
            ? presenter.CSS_CLASSES.CONNECTIONS_TOP_ROW
            : presenter.CSS_CLASSES.CONNECTIONS_LEFT_COLUMN;
    };

    presenter.getMiddleSideCSSClassName = function () {
        return presenter.isHorizontal
            ? presenter.CSS_CLASSES.CONNECTIONS_MIDDLE_ROW
            : presenter.CSS_CLASSES.CONNECTIONS_MIDDLE_COLUMN;
    };

    presenter.getSecondSideCSSClassName = function () {
        return presenter.isHorizontal
            ? presenter.CSS_CLASSES.CONNECTIONS_BOTTOM_ROW
            : presenter.CSS_CLASSES.CONNECTIONS_RIGHT_COLUMN;
    };

    function basicClickLogic(element) {
        if (!isEnabledOrMultiLineMode(element)) {
            return false;
        }

        // workaround for android webView
        // http://code.google.com/p/android/issues/detail?id=38808
        var current = new Date().getTime();
        var delta = current - presenter.lastClickTime;
        if (!isSelectionPossible || delta &lt; 50) return;
        presenter.lastClickTime = current;
        if (!$(element).hasClass(presenter.CSS_CLASSES.SELECTED) &amp;&amp; selectedItem == null) { // first element selected
            $(element).parent().find('.connectionItem').removeClass(presenter.CSS_CLASSES.SELECTED);
            $(element).addClass(presenter.CSS_CLASSES.SELECTED);
            selectedItem = $(element);
            return;
        }
        if (selectedItem != null &amp;&amp; $(element).get(0) == selectedItem.get(0)) { // clicking the selected element again
            $(element).removeClass(presenter.CSS_CLASSES.SELECTED);
            selectedItem = null;
            return;
        }
        const firstSideClassName = presenter.getFirstSideCSSClassName();
        const secondSideClassName = presenter.getSecondSideCSSClassName();
        if (selectedItem != null &amp;&amp;
            ($(element).parents(`.${firstSideClassName}`).get(0) == selectedItem.parents(`.${firstSideClassName}`).get(0) ||
                $(element).parents(`.${secondSideClassName}`).get(0) == selectedItem.parents(`.${secondSideClassName}`).get(0))) {
            // element clicked in the same column
            var linesToSwitch = [];
            if (presenter.configuration.isSingleConnectionMode) {
                for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
                    if (presenter.lineStack.get(i).connects(selectedItem)) {
                        linesToSwitch.push(presenter.lineStack.get(i))
                    }
                }
            }

            selectedItem.removeClass(presenter.CSS_CLASSES.SELECTED);
            if (linesToSwitch.length == 0) {
                $(element).addClass(presenter.CSS_CLASSES.SELECTED);
                selectedItem = $(element);
                return;
            } else {
                for (i in linesToSwitch) {
                    presenter.lineStack.remove(linesToSwitch[i]);
                    pushConnection(new Line($(element), linesToSwitch[i].otherSide(selectedItem)), false);
                }
            }
        } else {
            if (presenter.checkIfConnectionDisabled($(element).attr('id'), selectedItem.attr('id'))) {
                return;
            }
            var line = new Line($(element), selectedItem);
            var shouldDraw = true;

            if (presenter.configuration.isSingleConnectionMode) {
                var usedInLines = presenter.lineStack.isItemUsed(line);
                if (usedInLines.length === 2) {
                    shouldDraw = false
                }
            }
            if (shouldDraw) {
                pushConnection(line, false);
            }
        }

        selectedItem.removeClass('selected');
        selectedItem = null;
        return true;
    }

    presenter.drawTempLine = function (x, y) {
        if (presenter.$view.find('#connection_line_tmp').length &gt; 0) {
            presenter.$view.find('#connection_line_tmp').remove();
        }
        var m, angle, d, transform,
            x1 = parseInt(presenter.iconLeft, 10),
            y1 = parseInt(presenter.iconTop, 10);

        m = (y - y1) / (x - x1);
        angle = (Math.atan(m)) * 180 / (Math.PI);
        d = Math.sqrt(((x - x1) * (x - x1)) + ((y - y1) * (y - y1)));
        if (x &gt;= x1) {
            transform = (360 + angle) % 360;
        } else {
            transform = 180 + angle;
        }

        var div = $('&lt;div&gt;');
        div.attr('id', 'connection_line_tmp');
        div.attr('class', 'connection_line');
        div.attr('style', 'left: ' + x1 + 'px; top: ' + y1 + 'px');
        presenter.$view.prepend(div);
        presenter.$view.find('#connection_line_tmp').css({
            'left': x1,
            'top': y1,
            'width': d,
            'background-color': presenter.configuration.defaultConnectionColor,
            'transform': 'rotate(' + transform + 'deg)',
            'transform-origin': '0px 0px',
            '-ms-transform': 'rotate(' + transform + 'deg)',
            '-ms-transform-origin': '0px 0px',
            '-moz-transform': 'rotate(' + transform + 'deg)',
            '-moz-transform-origin': '0px 0px',
            '-webkit-transform': 'rotate(' + transform + 'deg)',
            '-webkit-transform-origin': '0px 0px',
            '-o-transform': 'rotate(' + transform + 'deg)',
            '-o-transform-origin': '0px 0px'
        });
    };

    presenter.registerListeners = function (view) {
        presenter.$connectionContainer = $(view).find('.' + presenter.CSS_CLASSES.CONNECTION_CONTAINER);
        presenter.$connectionContainer.click(function (e) {
            e.preventDefault();
            e.stopPropagation();
        });

        const $firstSide = $(view).find('.' + this.getFirstSideCSSClassName());
        $firstSide.click(function (e) {
            e.preventDefault();
            e.stopPropagation();
        });

        const $secondSide = $(view).find('.' + this.getSecondSideCSSClassName());
        $secondSide.click(function (e) {
            e.preventDefault();
            e.stopPropagation();
        });

        const $item = $(view).find('.' + presenter.CSS_CLASSES.CONNECTION_ITEM);
        $item.on('touchstart', function (e) {
            e.preventDefault();
            e.stopPropagation();
            presenter.lastEvent = e;
        });

        let scale = playerController.getScaleInformation();
        let draggedElementColumn;

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.2.2", "4.4.2"].indexOf(android_ver) === -1 || window.navigator.userAgent.indexOf('Chrome') &gt; 0) {
            $item.each(function () {
                var e = $(this);
                e.draggable({
                    revert: presenter.configuration.removeDraggedElement ? true : "invalid",
                    opacity: presenter.configuration.removeDraggedElement ? 1 : 0.7,
                    helper: presenter.configuration.removeDraggedElement ? "original" : "clone",
                    cursorAt: {
                        left: Math.round(e.find('.inner').width() / 2),
                        top: Math.round(e.find('.inner').height() / 2)
                    },
                    start: function (event, ui) {
                        if (!isEnabledOrMultiLineMode(this)) {
                            event.preventDefault();
                            return false;
                        }
                        ui.helper.css("visibility", "hidden");
                        ui.helper.find(".inner_addon").css("display","none");
                        var $iconWrapper = $(e).find("." + presenter.CSS_CLASSES.ICON_WRAPPER);
                        scale = playerController.getScaleInformation();

                        let height = $iconWrapper.height();
                        !presenter.isHorizontal &amp;&amp; (height /= 2);
                        let width = $iconWrapper.width();
                        presenter.isHorizontal &amp;&amp; (width /= 2);
                        presenter.iconTop = $iconWrapper.position().top / scale.scaleY + height;
                        presenter.iconLeft = $iconWrapper.position().left / scale.scaleX + width;

                        if (!isSelectionPossible) {
                            event.stopPropagation();
                            event.preventDefault();
                            return;
                        }
                        $item.removeClass(presenter.CSS_CLASSES.SELECTED);
                        selectedItem = null;
                        ui.helper.zIndex(100);
                        clickLogic(this);
                        if (presenter.configuration.removeDraggedElement) {
                            ui.helper.find('.icon').hide();
                            ui.helper.removeClass(presenter.CSS_CLASSES.SELECTED);
                        } else {
                            ui.helper.find('.icon').remove();
                            ui.helper.width($(this).find('.inner').width());
                            ui.helper.height($(this).find('.inner').height());
                        }
                        if ($(this).parents('.' + presenter.getFirstSideCSSClassName()).length) {
                            draggedElementColumn = 'left';
                        } else {
                            draggedElementColumn = 'right';
                        }
                    },
                    drag: function (event, ui) {
                        presenter.mouseSX = parseInt(event.pageX, 10) - parseInt(presenter.$view.offset().left, 10);
                        presenter.mouseSY = parseInt(event.pageY, 10) - parseInt(presenter.$view.offset().top, 10);

                        presenter.drawTempLine(presenter.mouseSX / scale.scaleX, presenter.mouseSY / scale.scaleY);
                    },
                    stop: function (event, ui) {
                        ui.helper.zIndex(0);
                        if (presenter.configuration.removeDraggedElement) {
                            ui.helper.find('.icon').show();
                        } else {
                            ui.helper.remove();
                        }
                        presenter.redraw();
                        if (presenter.$view.find('#connection_line_tmp').length &gt; 0) {
                            presenter.$view.find('#connection_line_tmp').remove();
                        }
                    }
                });

                e.droppable({
                    tolerance: "pointer",
                    drop: function (event, ui) {
                        $(this).removeClass(presenter.CSS_CLASSES.SELECTED);
                        basicClickLogic(this);
                        ui.draggable.removeClass(presenter.CSS_CLASSES.SELECTED);
                        if (presenter.lastEvent) {
                            presenter.lastEvent.type = "touchend";
                        }
                    },
                    over: function (event, ui) {
                        var elementColumn;
                        if ($(this).parents('.' + presenter.getFirstSideCSSClassName()).length) {
                            elementColumn = 'left';
                        } else {
                            elementColumn = 'right';
                        }
                        if (elementColumn != draggedElementColumn) {
                            $(this).addClass(presenter.CSS_CLASSES.SELECTED);
                        }
                    },
                    out: function (event, ui) {
                        $(this).removeClass(presenter.CSS_CLASSES.SELECTED);
                    }
                });
            });
        } else {
            $item.on('mouseleave', function (e) {
                e.stopPropagation();
            });

            $item.on('mouseenter', function (e) {
                e.stopPropagation();
            });

            $item.on('mouseup', function (e) {
                e.stopPropagation();
            });

            $item.on('mousedown', function (e) {
                e.stopPropagation();
            });

            $item.on('mouseover', function (e) {
                e.stopPropagation();
            });

            $item.on('mouseout', function (e) {
                e.stopPropagation();
            });
        }
        $item.on('touchend', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (presenter.lastEvent.type != e.type) {
                presenter.isClicked = false;
                clickLogic(this);
                presenter.isClicked = true;
            }
        });

        $item.click(function (e) {
            e.stopPropagation();
            if (!presenter.isClicked) {
                clickLogic(this);
            }
        });
    };

    function isAnswersEqualToCorrectResults (userAnswers, correctAnswers) {
        if (userAnswers == null || correctAnswers == null) return false;
        if (userAnswers.length !== correctAnswers.length) return false;

        var userAnswersParsed = [];
        var correctAnswersParsed = []
        userAnswers.forEach(function(element, index) {
            userAnswersParsed.push(element.join(':'));
        }, userAnswers);
        correctAnswers.forEach(function(element, index) {
            correctAnswersParsed.push(element.id + ':' + element.answer);
        }, correctAnswers);
      
        userAnswersParsed.sort();
        correctAnswersParsed.sort();
      
        for (var i = 0; i &lt; userAnswersParsed.length; ++i) {
          if (userAnswersParsed[i] !== correctAnswersParsed[i]) {
            return false;
          }
        }
        return true;
      }

    function appendAnswerDiv (column, elementId, answerDiv) {
        var element = $('&lt;table class="answerItem" id="connection-' + elementId + '"&gt;&lt;/div&gt;');
        var row = $('&lt;tr&gt;&lt;/tr&gt;');
        var innerElement = $('&lt;td class="inner"&gt;&lt;/td&gt;');
        var newRow = $('&lt;tr&gt;&lt;/tr&gt;');
        var newCell = $('&lt;td class="connectionItemWrapper"&gt;&lt;/td&gt;');
        innerElement.append(answerDiv);
        row.append(innerElement);
        element.append(row);
        newCell.append(element);
        newRow.append(newCell);
        column.append(newRow);
    }

    presenter.addAnswersElements = function (view, model, correctAnswers) {
        // left side
        userAnswers = []
        if (presenter.printableState.id.length &gt; 0) {
            for (var i = 0; i &lt; presenter.printableState.id.length; i++) {
                var pair = presenter.printableState.id[i].split(':');
                userAnswers.push(pair);
            }
        }

        var leftAnswerColumn = $(view).find('.answersLeftColumn:first').find('.content:first');
        var rightAnswerColumn = $(view).find('.answersRightColumn:first').find('.content:first');
        
        // Left answer column
        for (var i = 0; i &lt; model["Left column"].length; i++) {
            var element = presenter.elements[i];
            var leftId = element.id;
            var userConnectionsWithLeftId = userAnswers.filter((userAnswer) =&gt; {
                return(userAnswer[0] == leftId);
            })
            if (userConnectionsWithLeftId.length == 0) {
                var emptyAnswer = [`${leftId}`, ''];
                userConnectionsWithLeftId.push(emptyAnswer);
            }
            var correctConnectionsWithLeftId = correctAnswers.filter((correctAnswer) =&gt; {
                return(correctAnswer.id == leftId);
            });
            var answerDiv = isAnswersEqualToCorrectResults(userConnectionsWithLeftId, correctConnectionsWithLeftId) ?
                    $('&lt;div class="correctAnswerDiv"&gt;&lt;/div&gt;') :
                    $('&lt;div class="inCorrectAnswerDiv"&gt;&lt;/div&gt;');
            appendAnswerDiv(leftAnswerColumn, leftId, answerDiv);
        }
        // Right answer column
        for (var i = 0; i &lt; model["Right column"].length; i++) {
            var rightId = model["Right column"][i].id;
            var userConnectionsWithRightId = userAnswers.filter((userAnswer) =&gt; {
                return(userAnswer[1] == rightId);
            })
            var correctConnectionsWithRightId = correctAnswers.filter((correctAnswer) =&gt; {
                return(correctAnswer.answer == rightId);
            })
            var answerDiv = isAnswersEqualToCorrectResults(userConnectionsWithRightId, correctConnectionsWithRightId) ?
                $('&lt;div class="correctAnswerDiv"&gt;&lt;/div&gt;') :
                $('&lt;div class="inCorrectAnswerDiv"&gt;&lt;/div&gt;');
            appendAnswerDiv(rightAnswerColumn, rightId, answerDiv);
        }
    };

    presenter.isIDUnique = function (id) {
        if (id == '') return true;
        if ($.inArray(id, presenter.uniqueIDs) &lt; 0) {
            presenter.uniqueIDs.push(id);
            return true;
        } else {
            return false;
        }
    };

    presenter.loadElements = function (view, model) {
        loadElementsToFirstSide(view, model);
        loadElementsToSecondSide(view, model);
    };

    function loadElementsToFirstSide(view, model) {
        loadElementsToSide(view, model, true);
    }

    function loadElementsToSecondSide(view, model) {
        loadElementsToSide(view, model, false);
    }

    function loadElementsToSide(view, model, isFirstSide) {
        const parentToLoadElements = findParentToLoadElements(view, isFirstSide);

        const itemsModelKey = isFirstSide ? "Left column" : "Right column";
        const itemsModel = model[itemsModelKey];
        const itemsInOneSideNumber = itemsModel.length;
        presenter.columnSizes[itemsModelKey] = model[itemsModelKey].length;

        const isRandomOrderKey = isFirstSide ? "Random order left column" : "Random order right column";
        const isRandomOrder = ModelValidationUtils.validateBoolean(model[isRandomOrderKey]);

        let rowToLoad;
        const orderedIndexes = getOrderedIndexes(itemsInOneSideNumber, isRandomOrder);
        for (let nextIndex, i = 0; i &lt; itemsInOneSideNumber; i++) {
            nextIndex = orderedIndexes[i];
            if (!presenter.isIDUnique(itemsModel[nextIndex]["id"])) {
                return presenter.showErrorMessage("ID not unique");
            }

            if (!presenter.isHorizontal || i === 0) {
                rowToLoad = document.createElement("tr");
                parentToLoadElements.append(rowToLoad);
            }

            loadElement(itemsModel[nextIndex], rowToLoad, isFirstSide);
        }
    }

    function findParentToLoadElements(view, isFirstSide) {
        let parentClassName = presenter.isHorizontal
            ? (isFirstSide
                ? presenter.CSS_CLASSES.CONNECTIONS_TOP_ROW
                : presenter.CSS_CLASSES.CONNECTIONS_BOTTOM_ROW)
            : (isFirstSide
                ? presenter.CSS_CLASSES.CONNECTIONS_LEFT_COLUMN
                : presenter.CSS_CLASSES.CONNECTIONS_RIGHT_COLUMN
            );
        return $(view)
            .find('.' + parentClassName + ':first')
            .find('.' + presenter.CSS_CLASSES.CONTENT + ':first')
            .find('tbody:first')[0];
    }

    function getOrderedIndexes(size, isRandomOrder) {
        let orderedIndexes = [];
        if (isRandomOrder) {
            let counter = 0;
            while (counter &lt; size) {
                let nextIndex = nextInt(size);
                if (orderedIndexes.includes(nextIndex)) {
                    continue;
                }
                orderedIndexes.push(nextIndex);
                counter++;
            }
        } else {
            for (let i = 0; i &lt; size; i++) {
                orderedIndexes.push(i);
            }
        }
        return orderedIndexes;
    }

    function nextInt(upperBound) {
        if (presenter.printableController) {
            return presenter.printableController.nextInt(upperBound);
        }
        return Math.floor((Math.random() * upperBound))
    }

    function loadElement(itemModel, parentElement, isFirstSide) {
        const newCell = document.createElement("td");
        newCell.classList.add(presenter.CSS_CLASSES.CONNECTION_ITEM_WRAPPER);
        parentElement.append(newCell);

        const newItem = createConnectionItem(itemModel, isFirstSide);
        newCell.append(newItem);
    }

    function createConnectionItem(itemModel, isFirstSection) {
        const item = document.createElement("table");
        item.classList.add(presenter.CSS_CLASSES.CONNECTION_ITEM);
        item.id = "connection-" + itemModel["id"];

        const itemBody = document.createElement("tbody");
        item.append(itemBody);

        const contentCell = createConnectionItemContentCell(itemModel["content"], itemModel["additional class"]);
        const iconCell = createConnectionItemIconCell();
        if (presenter.isHorizontal) {
            const contentRow = document.createElement("tr");
            contentRow.append(contentCell);
            const iconRow = document.createElement("tr");
            iconRow.append(iconCell);
            if (isFirstSection) {
                itemBody.append(contentRow);
                itemBody.append(iconRow);
            } else {
                itemBody.append(iconRow);
                itemBody.append(contentRow);
            }
        } else {
            const sharedRow = document.createElement("tr");
            itemBody.append(sharedRow);
            if (isFirstSection) {
                sharedRow.append(contentCell);
                sharedRow.append(iconCell);
            } else {
                sharedRow.append(iconCell);
                sharedRow.append(contentCell);
            }
        }

        presenter.elements.push({
            element: $(item),
            id: itemModel["id"],
            connects: itemModel["connects to"]
        });

        return item;
    }

    function createConnectionItemContentCell(content, additionalClassName) {
        const contentElement = document.createElement("td");
        contentElement.classList.add(presenter.CSS_CLASSES.INNER);

        const contentWrapper = document.createElement("div");
        contentWrapper.classList.add(presenter.CSS_CLASSES.INNER_WRAPPER);
        contentWrapper.style.direction = isRTL ? "rtl" : "ltr";
        const parsedContent = presenter.textParser.parse(content);
        contentWrapper.innerHTML = window.xssUtils.sanitize(parsedContent);
        !!additionalClassName &amp;&amp; contentWrapper.classList.add(additionalClassName);
        presenter.configuration.isTabindexEnabled &amp;&amp; contentWrapper.setAttribute("tabindex", 0);
        contentElement.append(contentWrapper);

        return contentElement;
    }

    function createConnectionItemIconCell() {
        const iconElement = document.createElement("td");
        iconElement.classList.add(presenter.CSS_CLASSES.ICON);

        const iconWrapper = document.createElement("div");
        iconWrapper.classList.add(presenter.CSS_CLASSES.ICON_WRAPPER);
        iconElement.append(iconWrapper);

        return iconElement;
    }

    presenter.initializeView = function (view, model) {
        const $firstSideElement = $(view).find("." + this.getFirstSideCSSClassName() + ":first");
        const firstSideSize = {
            height: $firstSideElement.outerHeight(true),
            width: $firstSideElement.outerWidth(true),
        };

        const $secondSideElement = $(view).find("." + this.getSecondSideCSSClassName() + ":first");
        const secondSideSize = {
            height: $secondSideElement.outerHeight(true),
            width: $secondSideElement.outerWidth(true),
        };

        let height = model["Height"];// leftColumnHeight &gt; rightColumnHeight ? leftColumnHeight : rightColumnHeight;
        let width = model["Width"];
        if (presenter.isHorizontal) {
            height -= (firstSideSize.height + secondSideSize.height);
        } else {
            width -= (firstSideSize.width + secondSideSize.width);
        }

        presenter.height = height;
        presenter.width = width;

        const context = connections[0].getContext('2d');
        context.canvas.width = width;
        context.canvas.height = height;
        connections.css({
            width: width + "px",
            height: height + "px"
        });
        connections.translateCanvas({
            x: 0.5, y: 0.5
        });
    };

    presenter.gatherCorrectConnections = function () {
        presenter.correctConnections.clear();
        var elements = presenter.elements;
        for (var i = 0, elementsLength = elements.length; i &lt; elementsLength; i++) {
            var connects = elements[i]['connects'].split(',');
            for (var j = 0; j &lt; connects.length; j++) {
                if (connects[j] != "" &amp;&amp;
                    $.inArray(connects[j], presenter.uniqueIDs) &gt;= 0) {
                    var pair = [elements[i]['id'], connects[j]];
                    var line = new Line(
                        getElementById(pair[0]),
                        getElementById(pair[1])
                    );
                    if (!presenter.correctConnections.hasPair(pair)) {
                        presenter.correctConnections.push(line);
                    }
                }
            }
        }
    };

    presenter.drawConfiguredConnections = function () {
        for (var i = 0; i &lt; presenter.correctConnections.length(); i++) {
            pushConnection(presenter.correctConnections.get(i), true)
        }
        presenter.redraw();
    };

    presenter.getElementSnapPoint = function AddonConnection_getElementSnapPoint($element) {
        const offset = $element.offset();
        const scale = playerController ? playerController.getScaleInformation() : { scaleX: 1, scaleY: 1 };
        const isFirstSideElement = $element.parents('.' + presenter.getFirstSideCSSClassName()).length &gt; 0;

        let snapPoint;
        if (presenter.isHorizontal) {
            const elementWidth = $element.outerWidth() * scale.scaleX;
            const elementHeight = $element.outerHeight(true) * scale.scaleY;
            if (isFirstSideElement) {
                snapPoint = [offset.left + elementWidth / 2, offset.top + elementHeight];
            } else {
                snapPoint = [offset.left + elementWidth / 2, offset.top];
            }
        } else {
            const elementWidth = $element.outerWidth(true) * scale.scaleX;
            const elementHeight = $element.outerHeight() * scale.scaleY;
            if (isFirstSideElement) {
                snapPoint = [offset.left + elementWidth, offset.top + elementHeight / 2];
            } else {
                snapPoint = [offset.left, offset.top + elementHeight / 2];
            }
        }

        // snapPoint[0] is x offset, [1] is y offset
        snapPoint[0] /= scale.scaleX;
        snapPoint[1] /= scale.scaleY;

        return snapPoint;
    };

    function pushConnection(line, isPreview) {
        var addLine = true, linesToRemove = [], existingLines;
        if (presenter.configuration.isSingleConnectionMode) {
            existingLines = presenter.lineStack.isItemUsed(line);
            if (existingLines.length &gt; 0) {
                if (!isPreview) {
                    linesToRemove.push.apply(linesToRemove, existingLines)
                }
                if (presenter.lineStack.hasLine(line).length &gt; 0) {
                    addLine = false
                }
            }
        } else {
            existingLines = presenter.lineStack.hasLine(line);
            if (existingLines.length &gt; 0) {
                if (!isPreview) {
                    linesToRemove.push.apply(linesToRemove, existingLines);
                }
                addLine = false;
            }
        }
        for (var i = 0; i &lt; linesToRemove.length; i++) {
            presenter.lineStack.remove(linesToRemove[i]);
        }
        if (addLine) {
            presenter.lineStack.push(line);
        }
        readConnected(addLine);
    }

    presenter.redraw = function AddonConnection_redraw() {
        connections.width = connections.width;

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {
            presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTIONS).remove();
            var canvas2 = $('&lt;canvas&gt;&lt;/canvas&gt;');
            canvas2.addClass(presenter.CSS_CLASSES.CONNECTIONS);
            presenter.$view.find('.' + presenter.getMiddleSideCSSClassName()).append(canvas2);

            var context = canvas2[0].getContext('2d');
            context.canvas.width = presenter.width;
            context.canvas.height = presenter.height;
            canvas2.css({
                width: presenter.width + 'px',
                height: presenter.height + 'px'
            });
            canvas2.translateCanvas({
                x: 0.5, y: 0.5
            });

            connections = presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTIONS);
        } else {
            connections.clearCanvas();
        }

        for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
            drawLine(presenter.lineStack.get(i), presenter.configuration.defaultConnectionColor)
        }
    };

    presenter.redrawShowAnswers = function AddonConnection_redrawShowAnswers() {
        connections.clearCanvas();
        for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
            drawLine(presenter.lineStack.get(i), presenter.configuration.showAnswersLineColor);
        }
    };

    function drawLine(line, color) {
        if (line.isDisabled() &amp;&amp; presenter.configuration.disabledConnectionColor !== "") {
            color = presenter.configuration.disabledConnectionColor;
        }

        var from = presenter.getElementSnapPoint(line.from);
        var to = presenter.getElementSnapPoint(line.to);
        var canvasOffset = connections.offset();
        var scale = playerController ? playerController.getScaleInformation() : { scaleX: 1, scaleY: 1 };

        canvasOffset.left /= scale.scaleX;
        canvasOffset.top /= scale.scaleY;

        connections.drawLine({
            strokeStyle: color,
            strokeWidth: presenter.configuration.connectionThickness,
            x1: to[0] - canvasOffset.left,
            y1: to[1] - canvasOffset.top,
            x2: from[0] - canvasOffset.left,
            y2: from[1] - canvasOffset.top
        });
    }

    presenter.isShowingAnswers = function AddonConnection_isShowingAnswers(){
        return presenter.isGradualShowAnswersActive || presenter.isShowAnswersActive;
    }

    presenter.setShowErrorsMode = deferredCommandQueue.decorate(
        function () {
            presenter.isCheckActive = true;
            if (presenter.isShowAnswersActive) {
                presenter.hideAnswers();
            }
            if (presenter.configuration.isNotActivity) return 0;

            connections.clearCanvas();
            for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
                var line = presenter.lineStack.get(i);
                if (presenter.correctConnections.hasLine(line).length &gt; 0) {
                    drawLine(presenter.lineStack.get(i), presenter.configuration.correctConnectionColor);
                    var fromElementCorrect = presenter.$view.find('#' + presenter.lineStack.get(i).from[0].id);
                    var toElementCorrect = presenter.$view.find('#' + presenter.lineStack.get(i).to[0].id);
                    $(fromElementCorrect).addClass(CORRECT_ITEM_CLASS);
                    $(toElementCorrect).addClass(CORRECT_ITEM_CLASS);
                } else {
                    drawLine(presenter.lineStack.get(i), presenter.configuration.incorrectConnectionColor);
                    var fromElementIncorrect = presenter.$view.find('#' + presenter.lineStack.get(i).from[0].id);
                    var toElementIncorrect = presenter.$view.find('#' + presenter.lineStack.get(i).to[0].id);
                    $(fromElementIncorrect).addClass(WRONG_ITEM_CLASS);
                    $(toElementIncorrect).addClass(WRONG_ITEM_CLASS);
                }
            }
            presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTION_ITEM).each(function () {
                if ($(this).hasClass(CORRECT_ITEM_CLASS) &amp;&amp; $(this).hasClass(WRONG_ITEM_CLASS)) {
                    $(this).removeClass(CORRECT_ITEM_CLASS);
                }
            });
            presenter.$connectionContainer
                .find('.' + presenter.CSS_CLASSES.SELECTED)
                .removeClass(presenter.CSS_CLASSES.SELECTED);
            selectedItem = null;
            isSelectionPossible = false;
        }
    );

    presenter.setWorkMode = deferredCommandQueue.decorate(
        function () {
            presenter.isCheckActive = false;
            presenter.gatherCorrectConnections();
            presenter.redraw();
            presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTION_ITEM).each(function () {
                $(this).removeClass(CORRECT_ITEM_CLASS);
                $(this).removeClass(WRONG_ITEM_CLASS);
            });
            isSelectionPossible = true;
        }
    );

    presenter.reset = deferredCommandQueue.decorate(
        function (onlyWrongAnswers) {
            if (!presenter.configuration.isValid) {
                return;
            }

            if (presenter.isShowAnswersActive) {
                presenter.hideAnswers();
            }

            presenter.keyboardControllerObject.selectEnabled(true);

            if (!onlyWrongAnswers) {
                presenter.lineStack.clear();
            } else {
                this.lineStack.setSendEvents(false);
                var linesToRemove = [];
                for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
                    var line = presenter.lineStack.get(i);
                    if (presenter.correctConnections.hasLine(line).length === 0) {
                        linesToRemove.push(line)
                    }
                }

                linesToRemove.forEach(function (line) {
                    presenter.lineStack.remove(line);
                });
                this.lineStack.setSendEvents(true);
            }

            isSelectionPossible = true;
            presenter.$connectionContainer
                .find('.' + presenter.CSS_CLASSES.SELECTED)
                .removeClass(presenter.CSS_CLASSES.SELECTED);
            presenter.$view.find('.' + presenter.CSS_CLASSES.CONNECTION_ITEM).each(function () {
                $(this).removeClass(CORRECT_ITEM_CLASS);
                $(this).removeClass(WRONG_ITEM_CLASS);
            });

            presenter.redraw();
            presenter.setVisibility(presenter.isVisibleByDefault);
            presenter.isVisible = presenter.isVisibleByDefault;
            presenter.disabledConnections = [];
            if (!onlyWrongAnswers) {
                presenter.addDisabledElementsFromInitialValues();
                presenter.drawInitialValues();
            }
        }
    );

    // that method can return false results when called before mathjax is loaded, but cannot be moved to aysnc queue
    presenter.getErrorCount = function () {
        if (presenter.configuration.isNotActivity) return 0;

        var errors = 0;
        for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
            var line = presenter.lineStack.get(i);
            if (presenter.correctConnections.hasLine(line).length == 0) {
                errors++;
            }
        }
        return errors;
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isNotActivity) return 0;

        return presenter.correctConnections.length() - presenter.correctConnections.getDisabledCount();
    };

    // that method can return false results when called before mathjax is loaded, but cannot be moved to aysnc queue
    presenter.getScore = function () {
        if (presenter.configuration.isNotActivity) return 0;

        var score = 0;
        for (var i = 0; i &lt; presenter.lineStack.length(); i++) {
            var line = presenter.lineStack.get(i);
            if (presenter.correctConnections.hasLine(line).length &gt; 0) {
                if (!line.isDisabled()) {
                    score++;
                }
            }
        }
        return score;
    };

    presenter.gradualShowAnswers = function (itemIndex) {
        presenter.keyboardControllerObject.selectEnabled(false);
        presenter.addCurrentAnswersToTemporary();

        presenter.lineStack.clear();
        presenter.redraw();

        while(itemIndex &lt; presenter.GSAcounter || !presenter.elementIsShowAnswersViable(itemIndex)) {
            itemIndex++;
        }
        presenter.addCorrectAnswersToLineStack(itemIndex + 1);
        presenter.GSAcounter = itemIndex + 1;

        presenter.redrawShowAnswers();
        presenter.lineStack.clear();
        isSelectionPossible = false;

        presenter.restoreConnectionFromTemporary();
    }

    presenter.gradualHideAnswers = function () {
        presenter.isGradualShowAnswersActive = false;
        presenter.keyboardControllerObject.selectEnabled(true);
        presenter.redraw();
        isSelectionPossible = true;
        presenter.GSAcounter = 0;
    }

    presenter.elementIsShowAnswersViable = function(index) {
        var viableElement = false;
        var connects = presenter.elements[index].connects;
        for(var i = 0; i &lt; connects.length; i++) {
            if(connects[i] !== "" &amp;&amp; $.inArray(connects[i], presenter.uniqueIDs) &gt;= 0) {
                viableElement = true;
                break;
            }
        }
        return viableElement;
    }

    presenter.getActivitiesCount = function () {
        var counter = 0;
        for (var i = 0; i &lt; presenter.elements.length; i++) {
            if (presenter.elementIsShowAnswersViable(i)) counter++;
        }
        return counter;
    }

    presenter.getState = function () {
        // this is needed because run/setState method waits for MathJax process to be finished
        // if getState is called before MathJax EndProcess callback then state would be lost
        // this fix that problem
        if (!presenter.mathJaxLoaders.setStateLoaded &amp;&amp; presenter.initialState !== null) {
            return presenter.initialState;
        }

        var id = [];
        for (var i = 0; i &lt; presenter.lineStack.ids.length; i++) {
            id.push(presenter.lineStack.ids[i].join(':'))
        }
        return JSON.stringify({
            id: id,
            isVisible: presenter.isVisible
        });
    };

    presenter.setState = function (state) {
        var hookExecuted = false;
        presenter.initialState = state;
        presenter.mathJaxLoaders.setStateLoaded = false;

        presenter.mathJaxProcessEnded.then(function () {
            if (state !== '' &amp;&amp; !hookExecuted) {
                presenter.lineStack.setSendEvents(false);
                presenter.lineStack.clear();

                var parsedState = JSON.parse(state);
                var id;
                if (typeof parsedState.isVisible !== "undefined") {
                    id = parsedState.id;
                    presenter.setVisibility(parsedState.isVisible);
                    presenter.isVisible = parsedState.isVisible;
                } else {
                    id = parsedState;
                }
                for (var i = 0; i &lt; id.length; i++) {
                    var pair = id[i].split(':');
                    pushConnection(new Line(getElementById(pair[0]), getElementById(pair[1])), false);
                }

                presenter.lineStack.setSendEvents(true);
                presenter.redraw();
                presenter.mathJaxLoaders.setStateLoaded = true;
                deferredCommandQueue.resolve();
            }
            presenter.initialState = null;
            hookExecuted = true;
        });
    };

    presenter.validateAdditionalClass = function (view, additionalClass) {
        var additionalClassElements = $(view).find('.' + additionalClass);
        var isAdditionalClass = $(view).find('.' + additionalClass).length &gt; 0;

        if (!isAdditionalClass) {
            return {isPresent: false, count: 0};
        }

        return {isPresent: true, count: additionalClassElements.length};
    };

    presenter.validateView = function (view, searchingKeyword) {
        var validatedAdditionalClass = presenter.validateAdditionalClass(view, searchingKeyword);
        if (!validatedAdditionalClass.isPresent) {
            return {isPresent: false, count: validatedAdditionalClass.count};
        }
        return {isPresent: true, count: validatedAdditionalClass.count};
    };

    presenter.getElementById = function (id) {
        return getElementById(id);
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.isOK = function (source) {
        var selectedDestinations = getConnectedPoints(source);
        var correctDestinations = getCorrectPoints(source);
        var isCorrect = isSameArrays(selectedDestinations, correctDestinations);

        return {
            value: isCorrect,
            source: source,
            selectedDestinations: selectedDestinations,
            correctDestinations: correctDestinations
        };
    };

    function getConnectedPoints(source) {
        var connectedPoints = [];

        var selectedLines = presenter.lineStack.ids;
        for (var index = 0; index &lt; selectedLines.length; index++) {
            var selectedLinePoints = selectedLines[index];
            if (hasArrayElement(selectedLinePoints, source)) {
                var selectedDestinationPoint = selectedLinePoints[0] === source ? selectedLinePoints[1] : selectedLinePoints[0];
                connectedPoints.push(selectedDestinationPoint);
            }
        }

        return connectedPoints;
    }

    function getCorrectPoints(source) {
        var correctPoints = [];

        var correctLines = presenter.elements;
        for (var index = 0; index &lt; correctLines.length; index++) {
            var correctLine = correctLines[index];
            var correctLinePoints = correctLine.connects.split(',');
            if (correctLine.id === source) {
                correctPoints = correctPoints.concat(correctLinePoints);
            }
            if (hasArrayElement(correctLinePoints, source)) {
                correctPoints.push(correctLine.id)
            }
        }

        correctPoints = removeDuplicates(correctPoints);
        correctPoints = correctPoints.filter(function (value) {
            return value !== ";"
        });

        return correctPoints;
    }

    function removeDuplicates(correctPoints) {
        correctPoints = correctPoints.filter(function (value, index, arr) {
            return arr.indexOf(value) === index;
        });
        return correctPoints;
    }

    function isSameArrays(selectedDestinations, correctDestinations) {
        var serializedSelectedDestinations = selectedDestinations.sort().join(',');
        var serializedCorrectDestinations = correctDestinations.sort().join(',');

        return serializedSelectedDestinations === serializedCorrectDestinations;
    }

    function hasArrayElement(array, element) {
        for (var arrayIndex = 0; arrayIndex &lt; array.length; arrayIndex++)
            if (array[arrayIndex] === element)
                return true;

        return false;
    }

    presenter.isSelected = function (leftIndex, rightIndex) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var leftElement = getElementById(leftIndex);
        var rightElement = getElementById(rightIndex);
        var line = new Line(leftElement, rightElement);
        return (presenter.lineStack.hasLine(line).length &gt; 0);
    };

    presenter.isAttempted = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        return (presenter.lineStack.stack.length &gt; 0)
    };


    presenter.markAsCorrect = function (leftIndex, rightIndex) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var leftElement = getElementById(leftIndex);
        var rightElement = getElementById(rightIndex);
        var line = new Line(leftElement, rightElement);
        presenter.correctConnections.push(line);
        if (presenter.lineStack.hasLine(line))
            drawLine(line, presenter.configuration.correctConnectionColor);

    };

    presenter.markAsWrong = function (leftIndex, rightIndex) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var leftElement = getElementById(leftIndex);
        var rightElement = getElementById(rightIndex);
        var line = new Line(leftElement, rightElement);
        if (presenter.correctConnections.hasLine(line))
            presenter.correctConnections.remove(line);
        if (presenter.lineStack.hasLine(line))
            drawLine(line, presenter.configuration.incorrectConnectionColor);

    };

    presenter.isSelectedCommand = function (params) {
        presenter.isSelected(params[0], params[1]);
    };

    presenter.markAsCorrectCommand = function (params) {
        presenter.markAsCorrect(params[0], params[1]);
    };

    presenter.markAsWrongCommand = function (params) {
        presenter.markAsWrong(params[0], params[1]);
    };

    presenter.isAttemptedCommand = function () {
        return presenter.isAttempted();
    };

    presenter.executeCommand = function (name, params) {
        if (!isSelectionPossible) {
            return;
        }

        var commands = {
            'isAllOK': presenter.isAllOK,
            'isOK': presenter.isOK,
            'isSelected': presenter.isSelectedCommand,
            'markAsCorrect': presenter.markAsCorrectCommand,
            'markAsWrong': presenter.markAsWrongCommand,
            'isAttempted': presenter.isAttemptedCommand,
            'showAnswers': presenter.showAnswers,
            'show': presenter.show,
            'hide': presenter.hide,
            'hideAnswers': presenter.hideAnswers,
            'disable': presenter.disableCommand,
            'enable': presenter.enableCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.disableCommand = function (params) {
        if (params.length === 2) {
            presenter.disable(params[0], params[1]);
        }
    };

    presenter.disable = function (id1, id2) {
        presenter.disabledConnections.push({id1: id1, id2: id2});
    };

    presenter.enableCommand = function (params) {
        if (params.length === 2) {
            presenter.enable(params[0], params[1]);
        }
    };

    presenter.enable = function (id1, id2) {
        var convertedIds = convertIds(id1, id2);
        id1 = convertedIds.id1;
        id2 = convertedIds.id2;

        for (var i = 0; i &lt; presenter.disabledConnections.length; i++) {
            if ((presenter.disabledConnections[i].id1 == id1 &amp;&amp; presenter.disabledConnections[i].id2 == id2) ||
                (presenter.disabledConnections[i].id1 == id2 &amp;&amp; presenter.disabledConnections[i].id2 == id1)) {
                presenter.disabledConnections.splice(i, 1);
            }
        }
    };

    presenter.checkIfConnectionDisabled = function (id1, id2) {
        var convertedIds = convertIds(id1, id2);
        id1 = convertedIds.id1;
        id2 = convertedIds.id2;

        for (var i = 0; i &lt; presenter.disabledConnections.length; i++) {
            if ((presenter.disabledConnections[i].id1 == id1 &amp;&amp; presenter.disabledConnections[i].id2 == id2) ||
                (presenter.disabledConnections[i].id1 == id2 &amp;&amp; presenter.disabledConnections[i].id2 == id1)) {
                return true;
            }
        }

        return false;
    };

    presenter.onEventReceived = function (eventName, data) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        } else if (eventName === "GradualShowAnswers") {
            if (!presenter.isGradualShowAnswersActive) {
                presenter.isGradualShowAnswersActive = true;
            }
            if (data.moduleID === addonID) {
                presenter.gradualShowAnswers(parseInt(data.item, 10));
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.gradualHideAnswers();
        }
    };

    presenter.addCurrentAnswersToTemporary =function () {
        presenter.tmpElements = [];
        for (var elem = 0; elem &lt; presenter.lineStack.ids.length; elem++) {
            presenter.tmpElements.push(presenter.lineStack.ids[elem].join(':'))
        }
    }

    presenter.addCorrectAnswersToLineStack = function (length) {
        for (var i = 0; i &lt; length; i++) {
            var connects = presenter.elements[i]['connects'].split(',');
            for (var j = 0; j &lt; connects.length; j++) {
                if (connects[j] !== "" &amp;&amp; $.inArray(connects[j], presenter.uniqueIDs) &gt;= 0) {
                    var pair = [presenter.elements[i]['id'], connects[j]];
                    var line = new Line(
                        getElementById(pair[0]),
                        getElementById(pair[1])
                    );
                    presenter.lineStack.push(line);
                }
            }
        }

        presenter.lineStackSA = {
            stack: presenter.lineStack ? presenter.lineStack.stack.concat([]) : []
        };
    }

    presenter.restoreConnectionFromTemporary = function () {
        for (var element = 0; element &lt; presenter.tmpElements.length; element++) {
            var pairs = presenter.tmpElements[element].split(':');
            pushConnection(new Line(getElementById(pairs[0]), getElementById(pairs[1])), false);
        }
    }

    presenter.showAnswers = deferredCommandQueue.decorate(
        function () {
            if (presenter.configuration.isNotActivity) {
                return;
            }

            presenter.keyboardControllerObject.selectEnabled(false);
            presenter.isShowAnswersActive = true;
            presenter.addCurrentAnswersToTemporary();

            presenter.lineStack.clear();
            presenter.redraw();

            presenter.addCorrectAnswersToLineStack(presenter.elements.length)

            presenter.redrawShowAnswers();
            presenter.lineStack.clear();
            isSelectionPossible = false;

            presenter.restoreConnectionFromTemporary();
        }
    );

    presenter.hideAnswers = deferredCommandQueue.decorate(
        function () {
            if (presenter.configuration.isNotActivity || !presenter.isShowAnswersActive) {
                return;
            }
            presenter.keyboardControllerObject.selectEnabled(true);
            presenter.redraw();
            presenter.isShowAnswersActive = false;
            isSelectionPossible = true;
        }
    );

    presenter.keyboardController = function (keycode, isShiftDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftDown, event);
    };

    function ConnectionKeyboardController(elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function readConnected(isDrawing) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; presenter.$view.hasClass('ic_active_module')) {
            speak([getTextVoiceObject(
                isDrawing ? presenter.speechTexts.connected : presenter.speechTexts.disconnected
            )]);
        }
    }

    function getConnections($element) {
        var element = $element[0];
        var result = [];
        var lines = presenter.isShowingAnswers() ? presenter.lineStackSA : presenter.lineStack;

        for (var i = 0; i &lt; lines.stack.length; i++) {
            var line = lines.stack[i];

            if (element === line.from[0]) {
                result.push(line.to);
            }

            if (element === line.to[0]) {
                result.push(line.from);
            }
        }

        return result;
    }

    function getConnectionsInfo(connections) {
        var result = [];

        for (var i = 0; i &lt; connections.length; i++) {

            result = result.concat(window.TTSUtils.getTextVoiceArrayFromElement(connections[i], presenter.configuration.langTag));

            if (connections[i].hasClass(CORRECT_ITEM_CLASS) &amp;&amp; presenter.isCheckActive) {
                result.push(getTextVoiceObject(presenter.speechTexts.correct));
            }

            if (connections[i].hasClass(WRONG_ITEM_CLASS) &amp;&amp; presenter.isCheckActive) {
                result.push(getTextVoiceObject(presenter.speechTexts.wrong));
            }
        }

        return result;
    }

    function readActivatedElementConnections() {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts) {
            var $active = presenter.getCurrentActivatedElement();
            var connections = getConnections($active);
            var TextVoiceArray = window.TTSUtils.getTextVoiceArrayFromElement($active, presenter.configuration.langTag);

            if ($active.hasClass(presenter.CSS_CLASSES.SELECTED) &amp;&amp; !presenter.isShowingAnswers()) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.selected, ''));
            }

            if (connections.length) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.connectedTo, ''));
                TextVoiceArray = TextVoiceArray.concat(getConnectionsInfo(connections));
            }

            speak(TextVoiceArray);
        }
    }

    function speak(data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    ConnectionKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    ConnectionKeyboardController.prototype.constructor = ConnectionKeyboardController;

    ConnectionKeyboardController.prototype.nextRow = function (event) {
        event.preventDefault();
        var new_position_index = this.keyboardNavigationCurrentElementIndex + 1;
        if (new_position_index &gt;= this.keyboardNavigationElementsLen || new_position_index &lt; 0) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }
        if (new_position_index === presenter.columnSizes['Left column']) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }
        this.markCurrentElement(new_position_index);
        readActivatedElementConnections();
    };

    ConnectionKeyboardController.prototype.previousRow = function (event) {
        event.preventDefault();
        var new_position_index = this.keyboardNavigationCurrentElementIndex - 1;
        if (new_position_index &gt;= this.keyboardNavigationElementsLen || new_position_index &lt; 0) {
            new_position_index = this.keyboardNavigationCurrentElementIndex
        }
        if (new_position_index === presenter.columnSizes['Left column'] - 1) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }
        this.markCurrentElement(new_position_index);
        readActivatedElementConnections();
    };

    function indexesInTheSameColumn(index1, index2) {
        var leftColumnSize = presenter.columnSizes['Left column'];

        return (index1 &lt; leftColumnSize &amp;&amp; index2 &lt; leftColumnSize) || (index1 &gt;= leftColumnSize &amp;&amp; index2 &gt;= leftColumnSize);
    }

    ConnectionKeyboardController.prototype.nextElement = function () {
        var new_position_index = this.keyboardNavigationCurrentElementIndex + presenter.columnSizes['Left column'];

        if (new_position_index &gt;= this.keyboardNavigationElementsLen) {
            new_position_index = this.keyboardNavigationElementsLen - 1;
        }

        if (indexesInTheSameColumn(new_position_index, this.keyboardNavigationCurrentElementIndex)) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }

        this.markCurrentElement(new_position_index);
        readActivatedElementConnections();
    };

    ConnectionKeyboardController.prototype.previousElement = function () {
        var new_position_index = this.keyboardNavigationCurrentElementIndex - presenter.columnSizes['Right column'];

        if (new_position_index &lt; 0) {
            new_position_index = 0;
        }

        if (indexesInTheSameColumn(new_position_index, this.keyboardNavigationCurrentElementIndex)) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }

        this.markCurrentElement(new_position_index);
        readActivatedElementConnections();
    };

    ConnectionKeyboardController.prototype.enter = function (event) {
        if (event.shiftKey || event.ctrlKey) {
            Object.getPrototypeOf(ConnectionKeyboardController.prototype).escape.call(this);
        } else {
            Object.getPrototypeOf(ConnectionKeyboardController.prototype).enter.call(this);
            readActivatedElementConnections();
        }
    };

    ConnectionKeyboardController.prototype.select = function (event) {
        event.preventDefault();
        if (presenter.getCurrentActivatedElement().hasClass(presenter.CSS_CLASSES.SELECTED)) {
            speak([getTextVoiceObject(presenter.speechTexts.deselected)]);
        }

        Object.getPrototypeOf(ConnectionKeyboardController.prototype).select.call(this);

        if (presenter.getCurrentActivatedElement().hasClass(presenter.CSS_CLASSES.SELECTED)) {
            speak([getTextVoiceObject(presenter.speechTexts.selected)]);
        }
    };


    presenter.__internal__ = {
        Line: Line
    };

    function drawSVGLine($svg, firstID, secondID, correctLine, model) {
        var leftSize = model["Left column"].length;

        var isFirstIDInLeftColumn = false;
        var isFirstIDInRightColumn = false;
        var isSecondIDInLeftColumn = false;
        var isSecondIDInRightColumn = false;

        for (var i = 0; i &lt; presenter.elements.length; i++) {
            if (presenter.elements[i].id == firstID) {
                if (i &lt; leftSize) {
                    isFirstIDInLeftColumn = true;
                } else {
                    isFirstIDInRightColumn = true;
                }
            }
            if (presenter.elements[i].id == secondID) {
                if (i &lt; leftSize) {
                    isSecondIDInLeftColumn = true;
                } else {
                    isSecondIDInRightColumn = true;
                }
            }
        }

        if (
            (!isFirstIDInLeftColumn &amp;&amp; !isFirstIDInRightColumn)
            ||
            (!isSecondIDInLeftColumn &amp;&amp; !isSecondIDInRightColumn)) {
            return;
        }
        if (!isFirstIDInLeftColumn &amp;&amp; !isSecondIDInRightColumn
            &amp;&amp; isFirstIDInRightColumn &amp;&amp; isSecondIDInLeftColumn) {
            var tmp = secondID;
            secondID = firstID;
            firstID = tmp;
        }

        drawSVGLineLeftToRight($svg, firstID, secondID, correctLine, model);

    }

    function drawSVGLineLeftToRight($svg, leftID, rightID, correctLine, model) {
        var leftSize = model["Left column"].length;
        var leftTotalSize = 0;
        var rightTotalSize = 0;
        var leftPos = 0;
        var rightPos = 0;

        for (var i = 0; i &lt; presenter.elements.length; i++) {

            if (presenter.elements[i].id == leftID) {
                leftPos = leftTotalSize + presenter.elements[i].element.closest('tr').outerHeight(true) / 2;
            } else if (presenter.elements[i].id == rightID) {
                rightPos = rightTotalSize + presenter.elements[i].element.closest('tr').outerHeight(true) / 2;
            }

            if (i &lt; leftSize) {
                leftTotalSize += presenter.elements[i].element.closest('tr').outerHeight(true);
            } else {
                rightTotalSize += presenter.elements[i].element.closest('tr').outerHeight(true);
            }
        }
        var leftY = 100.0 * leftPos / leftTotalSize + "%";
        var rightY = 100.0 * rightPos / rightTotalSize + "%";
        var $line = correctLine
            ? $('&lt;line class="correctConnectionLine" x1="0" x2="100%" style="stroke: rgb(0, 0, 0); stroke-width: 1;" /&gt;')
            : $('&lt;line class="inCorrectConnectionLine" x1="0" x2="100%" style="stroke: rgb(0, 0, 0); stroke-width: 1;" stroke-dasharray="4" /&gt;');
        $line.attr('y1', leftY);
        $line.attr('y2', rightY);
        $svg.append($line);
    }

    presenter.setPrintableController = function (controller) {
        presenter.printableController = controller;

        presenter.textParser = new TextParserProxy(presenter.printableController.getTextParser());
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.isPrintableAsync = function() {
        return true;
    };

    presenter.setPrintableAsyncCallback = function(id, callback) {
        presenter.printableParserID = id;
        presenter.printableParserCallback = callback;
    };

    function getCorrectAnswersObject(model) {
        var correctAnswers = [];
        var idx = 0;
        for (var i = 0; i &lt; model["Left column"].length; i++) {
            var element = presenter.elements[i];
            var id = element.id;
            var correctAnswersValues = element.connects.split(',');
            if (correctAnswersValues.length == 0) {
                correctAnswers[idx] = {
                    'id': id,
                    'answer': null
                };
                idx++;
            } else {
                correctAnswersValues.forEach(function (answer) {
                    correctAnswers[idx] = {
                        'id': id,
                        'answer': answer
                    };
                    idx++;
                });
            }
        }
        return correctAnswers;
    }

    function isPrintableShowAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
    }
    function isPrintableShowUserAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
    }
    function isPrintableCheckAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
    }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        } else {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function isCorrectConnection (correctAnswers, connection) {
        for (var i = 0; i &lt; correctAnswers.length; i++) {
            var answer = correctAnswers[i];
            if (answer.id == connection[0] &amp;&amp; answer.answer == connection[1]) return true;
        }
        return false;
    }

    presenter.getPrintableHTML = function (model, showAnswers) {
        chosePrintableStateMode(showAnswers);
        model = presenter.upgradeModel(model);
        presenter.model = model;
        const view = createPrintableBaseView(model);
        const $view = $(view);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_MESSAGES, presenter.configuration.errorCode);
            return $view[0].outerHTML;
        }

        presenter.chooseOrientation(model, false);
        const connectionContainer = createPrintableConnectionContainer();
        view.append(connectionContainer);
        presenter.setLengthOfSideObjects(view);

        presenter.loadElements(view, model);
        this.removeNonVisibleInnerHTMLForRoot($view);

        const correctAnswers = getCorrectAnswersObject(model);
        const connectionsInformation = getPrintableConnectionsInformation(correctAnswers);
        isPrintableCheckAnswersStateMode() &amp;&amp; this.addAnswersElements(view, model, correctAnswers);

        const $printableWrapper = wrapInPrintableLessonTemplate($view);
        $printableWrapper.css("visibility", "hidden");
        $("body").append($printableWrapper);

        waitForLoad($view, function($elementWithRenderedMathJax){
            $printableWrapper.css("visibility", "");
            restoreRenderedMathJaxForPrintableView($view, $elementWithRenderedMathJax);
            forceBrowserToRecalculateSVGSize($view);
            drawConnectionsForPrintableView($view, model, connectionsInformation);
            setHeightForPrintableView(view);

            view.classList.remove(...presenter.printableClassNames);
            const printableHTML = view.outerHTML;

            $printableWrapper.remove();
            if (!!$elementWithRenderedMathJax) {
                $elementWithRenderedMathJax.remove();
            }

            presenter.printableParserCallback(printableHTML);
        });

        const $clone = $view.clone();
        $clone.attr("id", presenter.printableParserID);
        $clone.css("visibility", "");
        const result = $clone[0].outerHTML;
        $clone.remove();

        return result;
    };

    function setHeightForPrintableView(view) {
        let originalHeight = view.getBoundingClientRect().height;
        view.style.height = originalHeight + "px";

        let heightAfterChangeOfStyle = view.getBoundingClientRect().height;
        if (originalHeight !== heightAfterChangeOfStyle) {
            view.style.height = (originalHeight - (heightAfterChangeOfStyle - originalHeight)) + "px";
        }
    }

    function drawConnectionsForPrintableView($view, model, connectionsInformation) {
        if (connectionsInformation.length &gt; 0) {
            const $connectionsSVG = $view.find("svg");
            for (let connection, i = 0; i &lt; connectionsInformation.length; i++) {
                connection = connectionsInformation[i];
                drawSVGLine($connectionsSVG, connection.from, connection.to, connection.correct, model);
            }
        }
    }

    function restoreRenderedMathJaxForPrintableView($view, $viewWithRenderedMathJax) {
        if (!$viewWithRenderedMathJax) {
            return;
        }

        $view.find('[id^="MathJax-Element-"]').each(function(){
            this.classList.remove("MathJax_Processed");
            this.innerHTML = $viewWithRenderedMathJax.find("#" + this.id)[0].innerHTML;
        })
    }

    function forceBrowserToRecalculateSVGSize($view){
        const svgElement = $view.find("svg")[0];
        const svgElementParent = svgElement.parentElement;
        svgElementParent.innerHTML = svgElementParent.innerHTML;
    }

    presenter.setPrintableClassNames = function (classNames) {
        presenter.printableClassNames = classNames;
    }

    function wrapInPrintableLessonTemplate($view) {
        const $lessonTemplate = $(presenter.printableController.getLessonTemplate());
        const $placeholder = $lessonTemplate.find("#printable_placeholder");
        const $placeholderParent = $placeholder.parent();
        $placeholderParent.append($view);
        $placeholder.remove();
        return $lessonTemplate;
    }

    function createPrintableBaseView(model) {
        const view = document.createElement("div");
        view.id = model.ID;
        view.classList.add("printable_addon_Connection");
        view.style.maxWidth = model.Width + "px";
        view.classList.add(...presenter.printableClassNames);
        return view;
    }

    function createPrintableConnectionContainer() {
        const isCheckAnswers = isPrintableCheckAnswersStateMode();

        const connectionContainer = document.createElement("table");
        connectionContainer.classList.add("connectionContainer");

        const connectionContainerBody = document.createElement("tbody");
        connectionContainer.append(connectionContainerBody);

        const tr = document.createElement("tr");
        connectionContainerBody.append(tr);

        isCheckAnswers &amp;&amp; tr.append(createPrintableColumn("answersLeftColumn"));
        tr.append(createPrintableColumn(presenter.getFirstSideCSSClassName()));
        tr.append(createPrintableConnectionMiddleColumn());
        tr.append(createPrintableColumn(presenter.getSecondSideCSSClassName()));
        isCheckAnswers &amp;&amp; tr.append(createPrintableColumn("answersRightColumn"));

        return connectionContainer;
    }

    function createPrintableColumn(className) {
        const cellContent = createCellContent();
        return createVerticalViewBodyColumn(className, cellContent);
    }

    function createPrintableConnectionMiddleColumn() {
        const column = document.createElement("td");
        column.classList.add(presenter.getMiddleSideCSSClassName());

        const content = document.createElement("svg");
        content.classList.add(presenter.CSS_CLASSES.CONNECTIONS);

        column.append(content);
        return column;
    }

    function getPrintableConnectionsInformation(correctAnswers) {
        const information = [];

        if (isPrintableShowAnswersStateMode()) {
            for (let i = 0; i &lt; correctAnswers.length; i++) {
                if (correctAnswers[i].answer) {
                    information.push({
                        from: correctAnswers[i].id,
                        to: correctAnswers[i].answer,
                        correct: true
                    });
                }
            }
            return information;
        }

        if (isPrintableShowUserAnswersStateMode()) {
            for (let i = 0; i &lt; presenter.printableState.id.length; i++) {
                const pair = presenter.printableState.id[i].split(':');
                information.push({
                    from: pair[0],
                    to: pair[1],
                    correct: true
                });
            }
            return information;
        }

        if (isPrintableCheckAnswersStateMode()) {
            for (let i = 0; i &lt; presenter.printableState.id.length; i++) {
                const pair = presenter.printableState.id[i].split(':');
                information.push({
                    from: pair[0],
                    to: pair[1],
                    correct: isCorrectConnection(correctAnswers, pair)
                });
            }
            return information;
        }
        return information;
    }

    function waitForLoad($element, callback) {
        const $imgs = $element.find('img');
        let loadCounter = $imgs.length + 2;
        let continuedParsing = false;
        let isReady = false;
        let $elementWithRenderedMathJax = null;

        const mathJaxLoadedCallback = function(){
            $elementWithRenderedMathJax = $element.clone();
            loadCallback();
        };

        const loadCallback = function(){
            loadCounter -= 1;
            if (loadCounter &lt; 1 &amp;&amp; isReady &amp;&amp; !continuedParsing) {
                continuedParsing = true;
                if (timeout) clearTimeout(timeout);
                callback($elementWithRenderedMathJax);
            }
        };

        $imgs.each(function(){
            if (this.complete &amp;&amp; this.naturalHeight !== 0) {
                loadCallback();
            } else {
                $(this).load(loadCallback);
            }
        });

        $element.ready(function(){
            isReady = true;
            loadCallback();
        });

        const timeout = setTimeout(function(){
            if (loadCounter &gt; 0 || isReady === false) {
                isReady = true;
                loadCounter = 0;
                loadCallback();
            }
        }, 15000);

        const args = [];
        args.push("Typeset", MathJax.Hub, $element[0]);
        args.push(mathJaxLoadedCallback);
        MathJax.Hub.Queue(args);
    }

    return presenter;
}

AddonConnection_create.__supported_player_options__ = {
    resetInterfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Connector" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Tasks" nameLabel="Connector_property_tasks" type="list">
			<property name="Source" nameLabel="Connector_property_source" type="string"/>
			<property name="Item" nameLabel="Connector_property_item" type="string"/>
			<property name="Value" nameLabel="Connector_property_value" type="string"/>
			<property name="Score" nameLabel="Connector_property_score" type="string"/>
			<property name="Script" nameLabel="Connector_property_script" type="text"/>
		</property>
	</model>
<css/><view/><preview/><presenter>function AddonConnector_create () {
    var presenter = function () {};

    presenter.ERROR_MESSAGES = {
        VT_01: "Each task has to be assigned to source Addon!",
        VT_02: "Each task must have Script section filled!"
    };

    var playerController;
    var eventBus;
    var tasks = [];

    presenter.setPlayerController = function (controller) {
        playerController = controller;
    };

    presenter.onEventReceived = function (eventName, eventData) {
        var scripts = presenter.findMatchingTasks(tasks, eventData);

        for (var i = 0, length = scripts.length; i &lt; length; i += 1) {
            playerController.getCommands().executeEventCode(scripts[i]);
        }
    };

    presenter.createPreview = function (view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function (view, model) {
        presenterLogic(view, model, false);
    };

    function presenterLogic (view, model, isPreview) {
        var validatedTasks = presenter.validateTasks(model.Tasks);

        if (validatedTasks.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_MESSAGES, validatedTasks.errorCode);
            return;
        }

        if (isPreview) return;

        tasks = validatedTasks.tasks;
        eventBus = playerController.getEventBus();
        eventBus.addEventListener('ValueChanged', presenter);

        presenter.setVisibility(view, false);
    }

    presenter.setVisibility = function (view, isVisible) {
        $(view).css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.getTaskWithSource = function (tasks, source) {
        for (var i = 0, length = tasks.length; i &lt; length; i += 1) {
            if (tasks[i].source === source) return { index: i, tasks: tasks[i].tasks };
        }

        return { index: -1 };
    };

    presenter.addTask = function (tasks, task) {
        var index = presenter.getTaskWithSource(tasks, task.source).index;

        if (index === -1) {
            tasks.push(task);
        } else {
            var existingTask = tasks[index];

            for (var i = 0, length = task.tasks.length; i &lt; length; i += 1) {
                existingTask.tasks.push(task.tasks[i]);
            }

        }
    };

    presenter.validateTask = function (task) {
        var isEmpty = ModelValidationUtils.isArrayElementEmpty(task);

        if (!isEmpty &amp;&amp; !task.Source) return { isError: true, errorCode: "VT_01" };
        if (!isEmpty &amp;&amp; !task.Script) return { isError: true, errorCode: "VT_02" };

        return {
            isError: false,
            isEmpty: isEmpty,
            task: {
                source: task.Source,
                tasks: [{
                    item: task.Item,
                    value: task.Value,
                    score: task.Score,
                    script: task.Script
                }]
            }
        };
    };

    presenter.validateTasks = function (tasks) {
        if (ModelValidationUtils.isArrayEmpty(tasks)) return { tasks: [], isError: false };

        var tasksArray = [];
        var isError = false;

        for (var i = 0, length = tasks.length; i &lt; length; i += 1) {
            var validatedTask = presenter.validateTask(tasks[i]);

            if (validatedTask.isError) return { isError: true, errorCode: validatedTask.errorCode };

            presenter.addTask(tasksArray, validatedTask.task);
        }

        return { tasks: tasksArray, isError: isError };
    };

    presenter.FILTER_FIELD = {
        ITEM: 0,
        VALUE: 1,
        SCORE: 2
    };

    presenter.filterTasks = function (tasks, value, field) {
        var filteredTasks = [];

        for (var i = 0, length = tasks.length; i &lt; length; i += 1) {
            var fieldValue = "";
            switch (field) {
                case presenter.FILTER_FIELD.ITEM:
                    fieldValue = tasks[i].item;
                    break;
                case presenter.FILTER_FIELD.VALUE:
                    fieldValue = tasks[i].value;
                    break;
                case presenter.FILTER_FIELD.SCORE:
                    fieldValue = tasks[i].score;
                    break;
            }

            if (fieldValue !== '') {
                var isWildcardExpression = fieldValue.indexOf('*') !== -1;
                fieldValue = fieldValue.replace('*', '.*');
                var fieldRegExp = new RegExp(fieldValue);

                if (value.match(fieldRegExp)) {
                    if (isWildcardExpression) {
                        filteredTasks.push(tasks[i]);
                    } else  if (fieldValue.length === value.length) {
                        filteredTasks.push(tasks[i]);
                    }
                }
            } else {
                if (value === fieldValue) {
                    filteredTasks.push(tasks[i]);
                }
            }
        }

        return filteredTasks;
    };

    presenter.findMatchingTasks = function (tasks, eventData) {
        var matchingTasks = [];
        var i = 0, length = 0;


        var taskWithSource = presenter.getTaskWithSource(tasks, eventData.source);
        if (taskWithSource.index === -1) {
            return [];
        }

        for (i = 0, length = taskWithSource.tasks.length; i &lt; length; i += 1) {
            matchingTasks.push(taskWithSource.tasks[i]);
        }

        var tasksFilteredByItem = presenter.filterTasks(matchingTasks, eventData.item, presenter.FILTER_FIELD.ITEM);
        var tasksFilteredByValue = presenter.filterTasks(tasksFilteredByItem, eventData.value, presenter.FILTER_FIELD.VALUE);
        var tasksFilteredByScore = presenter.filterTasks(tasksFilteredByValue, eventData.score, presenter.FILTER_FIELD.SCORE);

        var scriptArray = [];
        for (i = 0, length = tasksFilteredByScore.length; i &lt; length; i += 1) {
            scriptArray.push(tasksFilteredByScore[i].script);
        }

        return scriptArray;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Count_and_Graph" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Is not an activity" name="isNotActivity" nameLabel="Count_and_Graph_property_is_not_activity" type="boolean"/>
        <property isLocalized="true" name="X axis description" nameLabel="Count_and_Graph_property_x_axis_description" type="string"/>
        <property name="X axis data" nameLabel="Count_and_Graph_property_x_axis_data" type="list">
            <property name="Answer" nameLabel="Count_and_Graph_property_answer" type="string"/>
            <property name="Color" nameLabel="Count_and_Graph_property_color" type="string"/>
            <property isLocalized="true" name="Description" nameLabel="Count_and_Graph_property_description" type="string"/>
            <property name="Description image" nameLabel="Count_and_Graph_property_description_image" type="image"/>
            <property name="Example" nameLabel="Count_and_Graph_property_example" type="boolean"/>
        </property>
        <property isLocalized="true" name="Y axis description" nameLabel="Count_and_Graph_property_y_axis_description" type="string"/>
        <property name="Y axis maximum value" nameLabel="Count_and_Graph_property_y_axis_minimum_value" type="string"/>
        <property name="Y axis values" nameLabel="Count_and_Graph_property_y_axis_values" type="string"/>
        <property name="Bars width" nameLabel="Count_and_Graph_property_bars_width" type="string"/>
        <property name="Background color" nameLabel="Count_and_Graph_property_background_color" type="string"/>
        <property name="Grid line color" nameLabel="Count_and_Graph_property_grid_line_color" type="string"/>
        <property name="Border" nameLabel="Count_and_Graph_property_border" type="string"/>
    </model>
<css>.main_container {
    position: relative;
    background-color: transparent;
    margin: 5px;
}

.upper_container {
    position: relative;
}

.bottom_container {

}

.graph_container {
    position: absolute;
    opacity: 0.8;
    top: 0px;
    left: 30px;
    box-shadow: 3px 0 15px -4px #333;
}

.column {
    background-color: white;
    position: absolute;
    opacity: 0.9;
    top: 0.5px;
    box-shadow: 3px 0 15px -4px #333, -3px 0 8px -4px #333;
}

.jqplot-xaxis-label {
    position: absolute;
}

.jqplot-xaxis {
    position: relative;
}

.jqplot-xaxis-tick {
    position: absolute;
    font-size: 8px;
    text-align: center;
    heigth: 20px;
    top: 10px;
}

.countGraph_image {
    position: absolute;
    width: 30px;
    height: 30px;
    margin-left: -15px;
}

.axis_x_dash {
    position: absolute;
    height: 5px;
    width: 1px;
    background-color: black;
}

.jqplot-yaxis-label {
    position: absolute;
    left: 0px;
    width: 30px;
    height: 30px;
}

.jqplot-yaxis-tick {
    position: absolute;
    font-size: 10px;
    left: 10px;
    height: 15px;
    vertical-align: middle;
}

.jqplot-yaxis {
    position: absolute;
    background-color: white;
    opacity: 0.8;
    top: 0px;
    left: 0px;
}

.axis_y_dash {
    position: absolute;
    left: 26px;
    height: 2px;
    width: 8px;
    background-color: black;
}
</css><view/><preview/><presenter>function AddonCount_and_Graph_create() {
    /*
        KNOWN ISSUES:
            Properties:
                Float user input - should be noticed but parsed to INT with parseInt, due to backward compatibility issue

                Axis Y Maximum Value:
                    Validation of property due to earlier versions of working &amp; invalid validation, converts floats to ints,
                    thats why it have to use parseFloat &amp; parseInt

                Axis Y Values:
                   Validation requires below requirements due to backward compatibility:
                   - Supported only 1 cyclic value
                   - cyclic value is every string ending with "*", and parseInt validated

                Bars width:
                    - Values between 0-1 are parsed to 1

                Border:
                    - negative numbers are ignored -&gt; no border
                    - strings are ignored -&gt; no border

                Axis X Data:
                    Answers values can be negative numbers or greater than axis Y max. It makes no sense, due to
                    addon activity is not solvable. Backward compatibility requires showing addon without being solvable.
                    Preview logic should show message  with addon, that addon configuration is invalid with answers.
    */

    var presenter = function () {};

    presenter.graph = null;
    presenter.isShowAnswersActive = false;
    presenter.errorMode = false;
    presenter.observer = undefined;

    presenter.ERROR_MESSAGES = {
        YAM_01: "Axis Y maximum value cant be lower or equal than 0.",
        YAM_02: "Axis Y maximum value have to be a integer string.",
        YAV_01: "Axis Y values cant be a non digit string.",
        YAV_02: "Axis Y cyclic value cant be equal or lower than 0.",
        YAV_03: "Axis Y fixed values cant be lower than 0.",
        YAV_04: "Axis Y values can\'t be greater than \"Axis Y Maximum Value\"",
        BW_01: "Bars width cant be lower than 0.",
        BW_02: "Bars width cant be a string.",
        BC_01: "Background color is not proper #RGB format.",
        AXD_01: "Axis data answers cant be empty string.",
        AXD_02: "Axis data answers have to be a positive integer value."
    };

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }
        presenter.$view.html(errorContainer);
    };

    presenter.getErrorObject = function (errorCode) {
        return { isValid: false, errorCode: errorCode };
    };

    presenter.GRAPH_EVENT_TYPE = {
        BAR_SELECTED: 0,
        IMAGE_HAS_FINISHED_LOADING: 1,
        GRAPH_HAS_FINISHED_LOADING: 2
    };

    presenter.barObject = function (parentColumn, color, width, height, barValue, columnIndex, isExample) {
        this._column = parentColumn;
        this._color = color;
        this._width = width;
        this._height = height;
        this._barValue = barValue;
        this._columnIndex = columnIndex;
        this._isExample = isExample;
        this.$view;
        this._$innerDiv;
        this._initView();
        this._connectEvents();
    };

    presenter.barObject.prototype._initView = function () {
        this.$view = $('&lt;div&gt;&lt;/div&gt;');
        this.$view.css({
            'background-color': 'white',
            'height': this._height + "px",
            'width': this._width + "px",
            'position': 'relative'
        });

        this._createInnerDivWithLabel();
    };

    presenter.barObject.prototype._createInnerDivWithLabel = function () {
        this._$innerDiv = $('&lt;div&gt;&lt;/div&gt;');
        this._$innerDiv.addClass(this._getInitialClassName());
        this._$innerDiv.css({
            'position': 'absolute',
            'height': "0px",
            'width': "0px",
            'top': (this._height / 2) + "px",
            'left': (this._width / 2) + "px"
        });

        this.$view.append(this._$innerDiv);
    };

    presenter.barObject.prototype._getInitialClassName = function () {
        return "jqplot-point-label jqplot-series-" + (this._barValue - 1) + " jqplot-point-" + this._columnIndex;
    };

    presenter.barObject.prototype.restoreInitialClassName = function () {
        this._$innerDiv.addClass(this._getInitialClassName());
    };

    presenter.barObject.prototype._connectEvents = function () {
        if (this._isExample) return;
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            this.$view[0].addEventListener("touchend", this, false);
        } else {
            this.$view[0].addEventListener("click", this, false);
        }
    };

    presenter.barObject.prototype.block = function () {
        if (this._isExample) return;
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            this.$view[0].removeEventListener("touchend", this, false);
        } else {
            this.$view[0].removeEventListener("click", this, false);
        }
    };

    presenter.barObject.prototype.unblock = function() {
        this._connectEvents()
    };

    presenter.barObject.prototype._notify = function (event) {
        this._column.update(event);
    };

    presenter.barObject.prototype.handleEvent = function (event) {
        event.preventDefault();
        event.stopPropagation();

        switch(event.type) {
            case "click":
                var barEvent = this._createSelectEvent();
                this._highLight();
                this._notify(barEvent);
                break;
            case "touchend":
                var barEvent = this._createSelectEvent();
                this._highLight();
                this._notify(barEvent);
                break;
        }
    };

    presenter.barObject.prototype._createSelectEvent = function () {
        return {
            type: presenter.GRAPH_EVENT_TYPE.BAR_SELECTED,
            barValue: this._barValue
        };
    };

    presenter.barObject.prototype._highLight = function () {
        this.$view.css('background-color', 'rgb(224, 223, 219)');
    };

    presenter.barObject.prototype.setOff = function() {
        this.$view.css('background-color', 'white');
    };

    presenter.barObject.prototype.setOn = function() {
        this.$view.css('background-color', this._color);
    };

    presenter.barObject.prototype.setCssClass = function (cssClass) {
        this._$innerDiv.addClass(cssClass);
    };

    presenter.barObject.prototype.removeCssClass = function (cssClass) {
        this._$innerDiv.removeClass(cssClass);
    };

    presenter.columnObject = function (axisYMaximumValue, color, columnWidth, columnHeight, answer, columnIndex, isExample) {
        this._axisYMaximumValue = axisYMaximumValue;
        this._barsNumber = axisYMaximumValue;
        this._barsColor = color;
        this._topSelectedBarNumber = -1;
        this._height = columnHeight;
        this._columnWidth = columnWidth;
        this._answer = answer;
        this._columnIndex = columnIndex;
        this.$view;
        this._bars;
        this.isExample = isExample;
        this._initializeColumn();
    };

    presenter.columnObject.prototype._initializeColumn = function () {
        this._bars = this._createBars();
        this._initView();
        this._appendBarsToColumn(this._bars);
        if (this.isExample) {
            this.showAnswer();
        }
    };

    presenter.columnObject.prototype._appendBarsToColumn = function (barsArray) {
        for (var i = barsArray.length - 1; i &gt;= 0; i--) {
            this.$view.append(barsArray[i].$view);
        }
    };

    presenter.columnObject.prototype._getBarWidth = function () {
        return this._columnWidth;
    };

    presenter.columnObject.prototype._getBarHeight = function () {
        return (this._height / this._barsNumber);
    };

    presenter.columnObject.prototype._initView = function () {
        this.$view = $('&lt;div&gt;&lt;/div&gt;');
        this.$view.addClass("column");
        this.$view.css({
            'height': this._height + "px",
            'width': this._columnWidth + "px"
        });
        if (this.isExample) {
            this.$view.addClass('example_column');
        }
    };

    presenter.columnObject.prototype.update = function (event) {
        switch(event.type) {
            case presenter.GRAPH_EVENT_TYPE.BAR_SELECTED:
                this._selectingBarLogic(event.barValue - 1);
                break;
        }
    };

    presenter.columnObject.prototype._createBars = function () {
        var bars = [];
        var barWidth = this._getBarWidth();
        var barHeight = this._getBarHeight();

        for(var i = 0; i &lt; this._barsNumber; i++) {
            var bar = new presenter.barObject(this, this._barsColor, barWidth, barHeight, i + 1, this._columnIndex, this.isExample);
            bars.push(bar);
        }

        return bars;
    };

    presenter.columnObject.prototype._selectingBarLogic = function (selectedBarIndex) {
        var previousValue = this._topSelectedBarNumber;
        var change = "";
        this._setOffSelectedBar(selectedBarIndex);

        if(this._columnShouldGetRaised(selectedBarIndex)) {
            this._raiseColumn();
            change = "increase";
        } else {
            this._lowerColumn();
            change = "decrease";
        }

        this._sendEvents(change, previousValue);
    };

    presenter.columnObject.prototype._sendEvents = function (changeType, previousValue) {
        if(this._isCorrect()) {
            presenter.sendEvent(true, this._columnIndex, changeType);
        } else {
            this._sendIncorrectEvent(previousValue + 1, changeType);
        }

        if(presenter.isAllOk()) {
            presenter.sendEventAllOk();
        }
    };

    presenter.columnObject.prototype._sendIncorrectEvent = function (previousValue, changeType) {
        if (previousValue &gt;= this._answer) {
            presenter.sendEvent(false, this._columnIndex, changeType);
        }
    };

    presenter.columnObject.prototype._isCorrect = function () {
        return ((this._topSelectedBarNumber + 1) == this._answer);
    };

    presenter.columnObject.prototype._setOffSelectedBar = function (selectedBarIndex) {
        if (selectedBarIndex &gt; this._topSelectedBarNumber) {
            this._setOffBar(selectedBarIndex);
        } else {
            this._setOnBar(selectedBarIndex);
        }
    };

    presenter.columnObject.prototype._columnShouldGetRaised = function (selectedBarIndex) {
        return (selectedBarIndex &gt; this._topSelectedBarNumber);
    };

    presenter.columnObject.prototype._setOffBar = function (barIndex) {
        this._bars[barIndex].setOff();
    };

    presenter.columnObject.prototype._lowerColumn = function () {
        this._setOffBar(this._topSelectedBarNumber);
        this._topSelectedBarNumber -= 1;
    };

    presenter.columnObject.prototype._raiseColumn = function () {
        this._setOnBar(this._topSelectedBarNumber + 1);
        this._topSelectedBarNumber += 1;
    };

    presenter.columnObject.prototype._setOnBar = function (barIndex) {
        this._bars[barIndex].setOn();
    };

    presenter.columnObject.prototype.setPosition = function (left) {
        this.$view.css({
            left: left + 'px'
        });
    };

    presenter.columnObject.prototype.getWidth = function () {
        return this.$view.width();
    };

    presenter.columnObject.prototype.showAnswer = function () {
        this.cleanSelection();
        this._drawCorrectAnswer();
    };

    presenter.columnObject.prototype.hideAnswer = function () {
        this.cleanSelection();
        this._drawUserAnswer();
    };

    presenter.columnObject.prototype._drawCorrectAnswer = function () {
        for(var i = 0; i &lt; this._answer; i++) {
            this._setOnBar(i);
        }
    };

    presenter.columnObject.prototype._drawUserAnswer = function () {
        for(var i = 0; i &lt;= this._topSelectedBarNumber; i++) {
            this._setOnBar(i);
        }
    };

    presenter.columnObject.prototype.block = function () {
        this._bars.forEach(function (element) {
            element.block();
        });
    };

    presenter.columnObject.prototype.unblock = function () {
        this._bars.forEach(function (element) {
            element.unblock();
        });
    };

    presenter.columnObject.prototype.cleanSelection = function () {
        this._bars.forEach(function (element) {
            element.setOff();
        });
    };

    presenter.columnObject.prototype._resetUserAnswer = function () {
        this._topSelectedBarNumber = -1;
    };

    presenter.columnObject.prototype.reset = function () {
        this.cleanSelection();
        this._resetUserAnswer();
    };

    presenter.columnObject.prototype.getScore = function () {
        if (this.isExample) return 0;

        if(this._isCorrect()) {
            return 1;
        }

        return 0;
    };

    presenter.columnObject.prototype.setShowErrorsMode = function () {
        if (this._topSelectedBarNumber &gt; -1) {
            var cssClass = this._getCssClassForCheckAnswers();

            this._setBarCssClass(this._topSelectedBarNumber, cssClass);
        }
    };

    presenter.columnObject.prototype._getCssClassForCheckAnswers = function () {
        return ("jqplot-point-label jqplot-series-" + this._topSelectedBarNumber + " jqplot-point-" + this._columnIndex +
                " " + this._getUserAnswerStatus());
    };

    presenter.columnObject.prototype._getUserAnswerStatus = function () {
        var userAnswer = this._topSelectedBarNumber + 1;

        if (userAnswer == this._answer) {
            return "ok";
        }

        if (userAnswer &gt; this._answer) {
            return "down";
        }

        return "up";
    };

    presenter.columnObject.prototype._setBarCssClass = function (barIndex, cssClass) {
        this._bars[barIndex].setCssClass(cssClass);
    };

    presenter.columnObject.prototype._removeBarCssClass = function (barIndex, cssClass) {
        this._bars[barIndex].removeCssClass(cssClass);
    };

    presenter.columnObject.prototype.setWorkMode = function () {
        if (this._topSelectedBarNumber &gt; -1) {
            var cssClass = this._getCssClassForCheckAnswers();

            this._removeBarCssClass(this._topSelectedBarNumber, cssClass);
            this._restoreBarInitialClassName(this._topSelectedBarNumber);
        }
    };

    presenter.columnObject.prototype._restoreBarInitialClassName = function (barIndex) {
        this._bars[barIndex].restoreInitialClassName();
    };

    presenter.columnObject.prototype.setState = function (userSelection) {
        this.reset();
        this._topSelectedBarNumber = (userSelection - 1);
        this._drawUserAnswer();
    };

    presenter.columnObject.prototype.getState = function () {
        if (this.isExample) return 0;
        return (this._topSelectedBarNumber + 1);
    };


    presenter.graphObject = function ($parentDiv, axisYMaximumValue, answers, colors, descriptions, imagesDescriptions,
                                      barsWidth, backgroundColor, gridLineColor, cyclicValue, fixedValues, axisSpace,
                                      border, axisYDescription, axisXDescription, exampleColumns, axisXImageHeight, axisXImageWidth) {
        this._axisSpace = axisSpace || 30;
        this._$parentDiv = $parentDiv;
        this._axisYMaximumValue = axisYMaximumValue;
        this._answers = answers;
        this._columnsNumber = answers.length;
        this._colors = colors;
        this._descriptions = descriptions;
        this._imagesDescriptions = imagesDescriptions;
        this._barsWidth = barsWidth;
        this._backgroundColor = backgroundColor;
        this._gridLineColor = gridLineColor;
        this._$graphContainer;
        this.$view;
        this.$upperContainer;
        this.$bottomContainer;
        this._$axisYContainer;
        this._$axisXContainer;
        this._columns;
        this._width = $parentDiv.width();
        this._height = $parentDiv.height();
        this._cyclicValue = cyclicValue;
        this._fixedValues = fixedValues;
        this._axisX;
        this._axisY;
        this._isBlocked = false;
        this._border = border;
        this._columnsAtMaxOffset = 10;
        this._axisXDescription = axisXDescription;
        this._axisYDescription = axisYDescription;
        this._axisXImageHeight = axisXImageHeight;
        this._axisXImageWidth =  axisXImageWidth;
        this._exampleColumns = [];
        this._exampleColumnsNumber = 0;
        if (exampleColumns != undefined) {
            this._exampleColumns = exampleColumns;
            for (var i = 0; i &lt; this._exampleColumns.length; i++) {
                if (this._exampleColumns[i]) this._exampleColumnsNumber += 1;
            }
        }
    };

    presenter.graphObject.prototype._getGraphWidth = function () {
        return (this._width - this._axisSpace);
    };

    presenter.graphObject.prototype._getGraphHeight = function () {
        if (MobileUtils.isSafariMobile(navigator.userAgent)) {
            return this._getIOSGraphHeight();
        } else {
            return this._getDefaultGraphHeight();
        }
    };

    presenter.graphObject.prototype._getDefaultGraphHeight = function () {
        return (this._height - this._axisSpace);
    };

    presenter.graphObject.prototype._getIOSGraphHeight = function () {
        var barHeight = this._getDefaultGraphHeight() / this._axisYMaximumValue;

        if ((barHeight % 1) != 0) {
            return (Math.floor(barHeight) * this._axisYMaximumValue);
        }

        return this._getDefaultGraphHeight();
    };

    presenter.graphObject.prototype._getAxisYWidth = function () {
        return this._axisSpace;
    };

    presenter.graphObject.prototype._getAxisYHeight = function () {
        return this._getGraphHeight();
    };

    presenter.graphObject.prototype._getAxisXHeight = function () {
        return (this._axisXImageHeight || this._axisSpace);
    };

    presenter.graphObject.prototype._createGrid = function ($graphContainer) {
        var width = $graphContainer.width();
        var height = $graphContainer.height();
        var gridStep = this._getGridLineStep(height);

        for (var i = 0, step = 0; i &lt;= this._axisYMaximumValue; i++, step += gridStep) {
            var $grid = this._getGridLine(width);
            $grid = this._positionGridLine($grid, step);
            $graphContainer.append($grid);
        }
    };

    presenter.graphObject.prototype._positionGridLine = function ($grid, top) {
        var topRepaired = top;

        if(top == 0) {
            topRepaired = 0;
        }

        $grid.css({
            'position': 'absolute',
            'top': topRepaired + 'px'
        });

        return $grid;
    };

    presenter.graphObject.prototype._getGridLineStep = function (height) {
        return (height / this._axisYMaximumValue);
    };

    presenter.graphObject.prototype._getGridLine = function (width) {
        var gridLine = $("&lt;div&gt;&lt;/div&gt;");

        gridLine.css({
            'border-top': '1px solid ' + this._gridLineColor,
            'width': width + "px",
            'left': 0 + "px",
            'height': 1 + "px"
        });

        return gridLine;
    };

    presenter.graphObject.prototype._createGraph = function () {
        this._$graphContainer = this._getGraphContainer(this._getGraphWidth(), this._getGraphHeight());

        this._createGrid(this._$graphContainer);

        this._columns = this._getColumns(this._getColumnWidth(), this._getGraphHeight());

        this._appendColumns(this._$graphContainer, this._columns);
        this._positionColumnsInGraph();

        return this._$graphContainer;
    };

    presenter.graphObject.prototype._positionColumnsInGraph = function () {
        var left = 0;
        var columnsMaxWidth = this._getColumnsMaxWidth();
        var rigth = columnsMaxWidth;
        for(var i = 0; i &lt; this._columns.length; i++) {

            var middleOfSection = this._getMiddleOfColumnSection(left, rigth, this._columns[i].getWidth());
            this._columns[i].setPosition(middleOfSection);

            left += columnsMaxWidth;
            rigth += columnsMaxWidth;
        }
    };

    presenter.graphObject.prototype._getBorderOffset = function () {
        // bars have 1px border, which creates offset for columns, and they dont fit into graph
        return (this._axisYMaximumValue - 1) * 2
    };

    presenter.graphObject.prototype._getMiddleOfColumnSection = function (left, right, columnsWidth) {
        var range = right - left;

        if(columnsWidth == range) {
            return left;
        }

        return ((range - columnsWidth) / 2) + left;
    };

    presenter.graphObject.prototype._getColumnsMaxWidth = function () {
        return (this._getGraphWidth() / this._columnsNumber);
    };

    presenter.graphObject.prototype._getColumnWidth = function () {
        if(this._barsWidth &gt; this._getColumnsMaxWidth()) {
            if ((this._getColumnsMaxWidth() - this._columnsAtMaxOffset) &lt; 0) {
                return 1;
            }

            return (this._getColumnsMaxWidth() - this._columnsAtMaxOffset);
        }

        return this._barsWidth;
    };

    presenter.graphObject.prototype._appendColumns = function ($container, columnsArray) {
        columnsArray.forEach(function (element) {
            $container.append(element.$view);
        });
    };

    presenter.graphObject.prototype._getGraphContainer = function (width, height) {
        var $graphContainer = $('&lt;div&gt;&lt;/div&gt;');
        $graphContainer.addClass("graph_container");
        $graphContainer.css({
            'background-color': this._backgroundColor + '',
            'height': height + "px",
            'width':  width + "px"
        });

        this._setGraphContainerBorder($graphContainer);

        return $graphContainer;
    };

    presenter.graphObject.prototype._setGraphContainerBorder = function ($graphContainer) {
        if(this._border &gt; 0) {
            if(this._border &gt; 3) {
                $graphContainer.css({
                    "border": "3px solid black",
                    "margin": "0px",
                    "padding": "0px"
                });
            } else {
                $graphContainer.css({
                    border: this._border + "px solid black",
                    "margin": "0px",
                    "padding": "0px"
                });
            }
        } else {
            $graphContainer.css({
                border: 0 + "px solid black",
                "margin": "0px",
                "padding": "0px"
            })
        }
    };

    presenter.graphObject.prototype._createAxisY = function () {
        var axisY = new presenter.axisYObject(this._axisYMaximumValue, this._getAxisYWidth(), this._getAxisYHeight(),
                                              this._cyclicValue, this._fixedValues, this._axisYDescription,
                                              this._gridLineColor);
        this._axisY = axisY;

        return axisY.$view;
    };

    presenter.graphObject.prototype._getColumnsDescriptionsPositions = function () {
        var values = [];
        var columnsMaxWidth = this._getColumnsMaxWidth();

        for(var i = 0, left = 0; i &lt; this._columnsNumber; i++, left += columnsMaxWidth) {
            values.push(left);
        }

        return values;
    };

    presenter.graphObject.prototype._createAxisX = function () {
        this._axisX = new presenter.axisXObject (
            this._getGraphWidth(), this._getAxisXHeight(), this._columnsNumber, this._descriptions, this._imagesDescriptions,
            this._getColumnsDescriptionsPositions(), this._getColumnsMaxWidth(), this._getColumnWidth(), this._axisXDescription,
            this._$graphContainer.css("border-bottom-width"), this._axisXImageWidth
        );

        return this._axisX.$view;
    };

    presenter.graphObject.prototype._getGraphHasFinishedLoadingEvent = function () {
        return {
            type: presenter.GRAPH_EVENT_TYPE.GRAPH_HAS_FINISHED_LOADING
        };
    };

    presenter.graphObject.prototype.notify = function () {
        var event = this._getGraphHasFinishedLoadingEvent();
        presenter.observer.update(event);
    };

    presenter.graphObject.prototype.setCallbackForAddonView = function () {
        this._$parentDiv.ready(function () {
            presenter.graph.notify();
        });
    };

    presenter.graphObject.prototype.initializeGraph = function () {
        this.$view = this._createMainContainer();

        this._$graphContainer = this._createGraph();
        this._appendGraph(this._$graphContainer);

        this._$axisYContainer = this._createAxisY();
        this._appendAxisY(this._$axisYContainer);

        this._$axisXContainer = this._createAxisX();
        this._appendAxisX(this._$axisXContainer);

        this._$parentDiv.append(this.$view);

        this.setCallbackForAddonView();
    };

    presenter.graphObject.prototype._appendAxisX = function ($axisXContainer) {
        this._setAxisXPosition($axisXContainer);
        this.$bottomContainer.append($axisXContainer);
    };

    presenter.graphObject.prototype._setAxisXPosition = function ($axisXContainer) {
        $axisXContainer.css({
            'left': this._axisSpace + "px"
        });
    };

    presenter.graphObject.prototype._appendGraph = function ($graphContainer) {
        this._setGraphPosition($graphContainer);
        this.$upperContainer.append($graphContainer);
    };

    presenter.graphObject.prototype._createMainContainer = function () {
        var $view = $('&lt;div&gt;&lt;/div&gt;');
        $view.addClass("main_container");
        $view.css({
            'height': this._height + "px",
            'width':  this._width + "px"
        });

        this.$upperContainer = $('&lt;div&gt;&lt;/div&gt;');
        this.$upperContainer.addClass("upper_container");
        this.$upperContainer.css({
            'width': this._width + "px",
            'height': this._getGraphHeight() + "px"
        });

        this.$bottomContainer = $('&lt;div&gt;&lt;/div&gt;');
        this.$bottomContainer.addClass("bottom_container");
        this.$bottomContainer.css({
            'width': this._width + "px",
            height: this._getAxisYWidth()
        });

        $view.append(this.$upperContainer);
        $view.append(this.$bottomContainer);

        return $view;
    };

    presenter.graphObject.prototype._setGraphPosition = function ($graphContainer) {

    };

    presenter.graphObject.prototype._appendAxisY = function ($axisYContainer) {
        this.$upperContainer.append($axisYContainer);
    };

    presenter.graphObject.prototype._getColumns = function (columnWidth, height) {
        var columns = [];
        for(var index = 0; index &lt; this._columnsNumber; index++) {
            var column = new presenter.columnObject(
                this._axisYMaximumValue, this._colors[index], columnWidth, height, this._answers[index], index, this._exampleColumns[index]
            );
            columns.push(column);
        }

        return columns;
    };

    presenter.graphObject.prototype.showAnswers = function () {
        this._columns.forEach(function (element) {
            if (!element.isExample) {
                element.showAnswer();
            }
        });
    };

    presenter.graphObject.prototype.gradualShowAnswers = function (item) {
        var exampleCount = 0;
        this._columns.forEach(function (element, index) {
            if (element.isExample) {
                exampleCount += 1;
            } else {
                if (index - exampleCount &lt;= item) {
                    element.showAnswer();
                } else {
                    element.cleanSelection();
                }
            }
        });
    };

    presenter.graphObject.prototype.hideAnswers = function () {
        this._columns.forEach(function (element) {
            if (!element.isExample) {
                element.hideAnswer();
            }
        });
    };

    presenter.graphObject.prototype.getActivitiesCount = function () {
        return this._columns.length - this._exampleColumnsNumber;
    };

    presenter.graphObject.prototype.reset = function () {
        this._columns.forEach(function (element) {
            if (!element.isExample) {
                element.reset();
            }
        });
    };

    presenter.graphObject.prototype.block = function () {
        if (!this._isBlocked) {
            this._columns.forEach(function (element) {
                element.block();
            });
            this._isBlocked = true;
        }
    };

    presenter.graphObject.prototype.unblock = function () {
        if (this._isBlocked) {
            this._columns.forEach(function (element) {
                element.unblock();
            });

            this._isBlocked = false;
        }
    };

    presenter.graphObject.prototype.getMaxScore = function () {
        return this._columnsNumber - this._exampleColumnsNumber;
    };

    presenter.graphObject.prototype.getScore = function () {
        return this._columns.map(function (column) {
            return column.getScore();
        }).reduce(function (previousValue, nextValue) {
            return previousValue + nextValue;
        });
    };

    presenter.graphObject.prototype.getErrorCount = function () {
        if(!this.isAttempted()) {
            return 0;
        }

        return (this.getMaxScore() - this.getScore());
    };

    presenter.graphObject.prototype.setShowErrorsMode = function () {
        this._columns.forEach(function (column) {
            column.setShowErrorsMode();
        });
    };

    presenter.graphObject.prototype.setWorkMode = function () {
        this._columns.forEach(function (column) {
            column.setWorkMode();
        });
    };

    presenter.graphObject.prototype.getState = function () {
        return this._columns.map(function(column) {
            return column.getState();
        });
    };

    presenter.graphObject.prototype.setState = function (userSelectionArray) {
        for(var i = 0; i &lt; userSelectionArray.length; i++) {
            if (!this._columns[i].isExample) {
                this._columns[i].setState(userSelectionArray[i]);
            }
        }
    };

    presenter.graphObject.prototype.getImagesHeight = function () {
        return this.$view.find(".countGraph_image").height();
    };

    presenter.graphObject.prototype.getImagesWidth = function () {
        return this.$view.find(".countGraph_image").width();
    };

    presenter.graphObject.prototype._setRescaledSizes = function () {
        this._axisXImageHeight = this.getImagesHeight() + this._axisSpace;
        this._axisXImageWidth = this.getImagesWidth();

        this._height -= this._axisXImageHeight;
    };

    presenter.graphObject.prototype.rescale = function () {
        this._setRescaledSizes();

        var currentState = this.getState();

        this.$view.remove();
        this.initializeGraph();

        this.setState(currentState);
    };

    presenter.axisYObject = function (axisYMaximumValue, width, height, cyclicValue, fixedValues, axisYDescription,
                                      axisYColor) {
        this._axisYMaximumValue = axisYMaximumValue;
        this._width = width;
        this._height = height;
        this._cyclicValue = cyclicValue;
        this._fixedValues = fixedValues;
        this._axisYDescription = axisYDescription;
        this._$axisYDescription = null;
        this._axisYColor = axisYColor;
        this.$view = null;
        this._init();
    };

    presenter.axisYObject.prototype._init = function () {
        this.$view = this._createAxisYContainer();
        this._createAxisYDescription();

        this._createTicks(this._getTicksValues());
    };

    presenter.axisYObject.prototype._createAxisYDescription = function () {
        this._$axisYDescription = this._getAxisYDescription();
        this._appendAxisYDescription(this._$axisYDescription);
    };

    presenter.axisYObject.prototype._getAxisYDescription = function () {
        var $div = $("&lt;div&gt;&lt;/div&gt;");
        $div.addClass("jqplot-yaxis-label");
        $div.css({
            top: ((this._height - 30) / 2) + "px"
        });

        $div.append(this._axisYDescription);

        return $div;
    };

    presenter.axisYObject.prototype._appendAxisYDescription = function ($axisDescription) {
        this.$view.append($axisDescription);
    };

    presenter.axisYObject.prototype._createTicks = function (tickValues) {
        var stepsValues = this._getReversedAxisYStepsValues();

        for(var i = 0; i &lt; tickValues.length; i++) {
            var value = tickValues[i];
            var $tick = this._getTick(value);
            this._appendTickToContainer($tick, value, stepsValues);
        }
    };

    presenter.axisYObject.prototype._getReversedAxisYStepsValues = function () {
        var step = this._getAxisYStep();
        var values = [];

        for(var i = 0; i &lt;= this._axisYMaximumValue; i++) {
            values.push((i * step));
        }

        return values.reverse();
    };

    presenter.axisYObject.prototype._getTick = function (value) {
        var $tick = $('&lt;div&gt;&lt;/div&gt;');
        $tick.addClass("jqplot-yaxis-tick");

        this._setTickCss($tick);

        $tick.append(value);

        return $tick;
    };

    presenter.axisYObject.prototype._setTickCss = function ($tick) {
        $tick.css({
           width: "10px"
        });
    };

    presenter.axisYObject.prototype._appendTickToContainer = function ($tick, value, reversedAxisYSteps) {
        this.$view.append($tick);

        $tick.css({
            'top': (reversedAxisYSteps[value] - 7.5) + "px"
        });
    };

    presenter.axisYObject.prototype._createTicksFromCyclicValue = function () {
        var ticks = [];
        var cyclicValue = this._cyclicValue[0];

        for(var i = 0; i &lt;= this._axisYMaximumValue; i += cyclicValue) {
            ticks.push(i);
        }

        return ticks;
    };

    presenter.axisYObject.prototype._getTicksValues = function () {
        if (this._fixedValues) {
            return this._fixedValues;
        } else {
            return this._createTicksFromCyclicValue();
        }
    };

    presenter.axisYObject.prototype._createAxisYContainer = function () {
        var $view = $('&lt;div&gt;&lt;/div&gt;');
        $view.addClass("jqplot-axis");
        $view.addClass("jqplot-yaxis");
        $view.css({
            'background-color': "transparent",
            'height': this._height + "px",
            'width':  this._width + "px",
            'border-right-style': "solid",
            'border-width': 1 + "px",
            'border-right-color': this._axisYColor
        });

        return $view;
    };

    presenter.axisYObject.prototype._getAxisYStep = function () {
        return (this._height) / this._axisYMaximumValue;
    };

    presenter.axisYObject.prototype._createAxisYLines = function () {
        var axisStep = this._getAxisYStep();

        var $firstDash = this._getDash();
        this._appendDashToContainer($firstDash, -0.5);

        for(var i = 1, step = axisStep; i &lt;= this._axisYMaximumValue; i++, step += axisStep) {
            var $dash = this._getDash();
            this._appendDashToContainer($dash, step);
        }
    };

    presenter.axisYObject.prototype._getDash = function () {
        var $dash = $("&lt;div&gt;&lt;/div&gt;");
        $dash.css({
            height: "2px",
            width: "8px",
            'background-color': "black"
        });

        return $dash;
    };

    presenter.axisYObject.prototype._appendDashToContainer = function ($dash, step) {
        this.$view.append($dash);
        $dash.css({
            position: "absolute",
            top: (step + 0.5) + "px",
            left: 26 + "px"
        });
    };

    presenter.axisXObject = function (width, height, numberOfColumns, seriesDescription, seriesImageDescriptions,
                                      columnsPositions, columnsMaxWidth, columnWidth, axisXDescription,
                                      graphContainerBorderWidth, imageLeftMargin) {
        this._width = width;
        this._height = height;
        this._numberOfColumns = numberOfColumns;
        this._seriesDescriptions = seriesDescription;
        this._seriesImageDescriptions = seriesImageDescriptions;
        this._columnsPositions = columnsPositions;
        this._columnsMaxWidth = columnsMaxWidth;
        this._columnWidth = columnWidth;
        this._axisXDescription = axisXDescription;
        this._graphContainerBorderWidth = parseInt(graphContainerBorderWidth, 10);
        this._$axisXDescription = null;
        this._imageLeftMargin = ((imageLeftMargin / 2) || 15);
        this.$view = null;
        this._initView();
    };

    presenter.axisXObject.prototype._initView = function () {
        this.$view = this._getMainContainer();
        this._createAxisXDescription();
        this._createDashes();
        this._createDescriptions();
    };

    presenter.axisXObject.prototype._createAxisXDescription = function () {
        this._$axisXDescription = this._getAxisXDescription();
        this._appendAxisXDescription(this._$axisXDescription);
    };

    presenter.axisXObject.prototype._getAxisXDescription = function () {
        var $div = $("&lt;div&gt;&lt;/div&gt;");

        $div.addClass("jqplot-xaxis-label");
        $div.css({
            top: (this._height - 5) + "px",
            height: 10 + "px",
            left: ((this._width - 30) / 2)  + "px"
        });

        $div.append(this._axisXDescription);

        return $div;
    };

    presenter.axisXObject.prototype._appendAxisXDescription = function ($axisDescription) {
        this.$view.append($axisDescription);
    };

    presenter.axisXObject.prototype._getMainContainer = function () {
        var $mainContainer = $("&lt;div&gt;&lt;/div&gt;");
        $mainContainer.addClass("jqplot-axis");
        $mainContainer.addClass("jqplot-xaxis");
        $mainContainer.css({
            'width': (this._width + 2) + "px",
            'height': this._height + "px",
            'background-color': "transparent"
        });

        return $mainContainer;
    };

    presenter.axisXObject.prototype._createDescriptions = function () {
        for(var i = 0; i &lt; this._seriesDescriptions.length; i++) {
            var $description;
            if (this._shouldCreateTextDescription(i)) {
                $description = this._getTextDescription(i);
            } else {
                $description = this._getImageDescription(i);
            }

            this._setDescriptionPosition($description, i);
        }
    };

    presenter.axisXObject.prototype._shouldCreateTextDescription = function (index) {
        return (this._seriesDescriptions[index].length &gt; 0);
    };

    presenter.axisXObject.prototype._getTextDescription = function (index) {
        var $description = $("&lt;div&gt;&lt;/div&gt;");
        $description.addClass("jqplot-xaxis-tick");
        $description.append(this._seriesDescriptions[index]);

        return $description;
    };

    presenter.axisXObject.prototype._getImageDescription = function (index) {
        var $description = $("&lt;div&gt;&lt;/div&gt;");
        $description.addClass("jqplot-xaxis-tick");

        if (this._seriesImageDescriptions[index].length &gt; 0) {
            var $image = $("&lt;img/&gt;");
            this._setCssToImageDescription($image, index);

            $description.append($image);
        }

        return $description;
    };

    presenter.axisXObject.prototype._setCssToImageDescription = function ($image, index) {
        $image.addClass("countGraph_image");
        this._addLoadingEventHandler($image);
        $image.attr('src', this._seriesImageDescriptions[index]);
        $image.css({
           "margin-left": -(this._imageLeftMargin) + "px"
        });
    };

    presenter.axisXObject.prototype._getImageHasFinishedLoadingEvent = function (width, height) {
        return {
            type: presenter.GRAPH_EVENT_TYPE.IMAGE_HAS_FINISHED_LOADING,
            data: {
                width: width,
                height: height
            }
        };
    };

    presenter.axisXObject.prototype._addLoadingEventHandler = function ($image) {

        var callbackFunction = this._notify;

        $image.on("load", function () {
            var event = this._getImageHasFinishedLoadingEvent($image.width(), $image.height());
            callbackFunction(event);
        }.bind(this));
    };

    presenter.axisXObject.prototype._notify = function (event) {
        presenter.observer.update(event);
    };

    presenter.axisXObject.prototype._setDescriptionPosition = function ($description, index) {
        this.$view.append($description);

        $description.css({
            'left': (this._columnsPositions[index]) + "px",
            'width': this._columnsMaxWidth + "px"
        });
    };

    presenter.axisXObject.prototype._createDashes = function () {
        for(var i = 0; i &lt; this._numberOfColumns + 1; i++) {
            var $dash = this._getVerticalDash();
            this.$view.append($dash);
            this._positionDash($dash, i * this._columnsMaxWidth);
        }
    };

    presenter.axisXObject.prototype._positionDash = function ($dash, position) {
        var top = 1 + this._graphContainerBorderWidth;
        $dash.css({
            'left': position + "px",
            'top': top + "px"
        });
    };

    presenter.axisXObject.prototype._getVerticalDash = function () {
        var $dash = $("&lt;div&gt;&lt;/div&gt;");
        $dash.addClass("axis_x_dash");

        return $dash;
    };

    presenter.graphObserver = function () {
        this._shouldGraphGetRescaled = false;
        this._imagesHeight = undefined;
        this._imagesHasFinishedLoading = false;
        this._graphHasFinishedLoading = false;
        this._graphHasBeenRescaled = false;
    };

    presenter.graphObserver.prototype.update = function (event) {
        switch(event.type) {
            case presenter.GRAPH_EVENT_TYPE.IMAGE_HAS_FINISHED_LOADING:
                this._imageHasFinishedLoadingHandler(event);
                break;
            case presenter.GRAPH_EVENT_TYPE.GRAPH_HAS_FINISHED_LOADING:
                this._graphHasFinishedLoadingHandler();
                break;
        }

        this._updateGraph();
    };

    presenter.graphObserver.prototype._setGraphShouldGetRescaled = function () {
        this._shouldGraphGetRescaled = this._areGraphActionsSynchronised() &amp;&amp; this.areImagesRescaled();
    };

    presenter.graphObserver.prototype._imageHasFinishedLoadingHandler = function (event) {
        if (!this._imagesHasFinishedLoading) {
            this._setObserverAfterImagesHasLoaded(event);
        }
    };

    presenter.graphObserver.prototype._setObserverAfterImagesHasLoaded = function (event) {
        this._imagesHasFinishedLoading = true;
        this._imagesHeight = event.data.height;
        this._setGraphShouldGetRescaled();
    };

    presenter.graphObserver.prototype._graphHasFinishedLoadingHandler = function () {
        if (!this._graphHasFinishedLoading) {
            this._setObserverAfterGraphHasLoaded();
        }
    };

    presenter.graphObserver.prototype._setObserverAfterGraphHasLoaded = function () {
        this._graphHasFinishedLoading = true;
        this._setGraphShouldGetRescaled();
    };

    presenter.graphObserver.prototype._updateGraph = function () {
        var task = this._getUpdateGraphTask();
        task.execute();
    };

    presenter.graphObserver.prototype._getUpdateGraphTask = function () {
        if (this._shouldCreateRescalingTask()) {
            return this._createRescalingTask();
        }
        return this._createNullTask();
    };

    presenter.graphObserver.prototype._shouldCreateRescalingTask = function () {
        return (this._shouldGraphGetRescaled &amp;&amp; !this._graphHasBeenRescaled);
    };

    presenter.graphObserver.prototype.setGraphHasBeenRescaled = function (graphHasBeenRescaled) {
        this._graphHasBeenRescaled = graphHasBeenRescaled;
    };

    presenter.graphObserver.prototype._createRescalingTask = function () {
        return {
            execute: function () {
                presenter.observer.setGraphHasBeenRescaled(true);
                presenter.graph.rescale();
            }
        };
    };

    presenter.graphObserver.prototype._createNullTask = function () {
        return {
            execute: function () {}
        };
    };

    presenter.graphObserver.prototype._areGraphActionsSynchronised = function () {
        return (this._graphHasFinishedLoading &amp;&amp; this._imagesHasFinishedLoading);
    };

    presenter.graphObserver.prototype.areImagesRescaled = function () {
        return this._areImagesRescaled(this._imagesHeight);
    };

    presenter.graphObserver.prototype._areImagesRescaled = function (height) {
        if (height == undefined) {
            return false;
        }

        return (height != 30);
    };

    presenter.run = function(view, model) {
        presenter.runLogic(view, model, false);

        presenter.addEventsListeners();
    };

    presenter.addEventsListeners = function () {
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('GradualShowAnswers', this);
        presenter.eventBus.addEventListener('GradualHideAnswers', this);
    };

    presenter.createPreview = function(view, model) {
        presenter.runLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        var validatedAxisYMaximumValue = presenter.validateAxisYMaximumValue(model);
        if (!validatedAxisYMaximumValue.isValid) {
            return validatedAxisYMaximumValue;
        }

        var validatedAxisYValues = presenter.validateAxisYValues(model, validatedAxisYMaximumValue.value);
        if (!validatedAxisYValues.isValid) {
            return validatedAxisYValues;
        }

        var validatedBarsWidth = presenter.validateBarsWidth(model);
        if (!validatedBarsWidth.isValid) {
            return validatedBarsWidth;
        }

        var validatedBackgroundColor = presenter.validateBackgroundColor(model);
        if (!validatedBackgroundColor.isValid) {
            return validatedBackgroundColor;
        }

        var validatedGridLineColor = presenter.validateGridLineColor(model);
        if (!validatedGridLineColor.isValid) {
            return validatedGridLineColor;
        }

        var validatedBorder = presenter.validateBorder(model);
        if (!validatedBorder.isValid) {
            return validatedBorder;
        }

        var validatedAxisXData = presenter.validateAxisXData(model, validatedAxisYMaximumValue.value);
        if (!validatedAxisXData.isValid) {
            return validatedAxisXData;
        }

        return {
            isValid: true,
            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            wasFloat: {
                axisYMaximumValue: validatedAxisYMaximumValue.wasFloat,
                axisYValues: validatedAxisYValues.wasFloat,
                axisXData: validatedAxisXData.wasFloat
            },
            axisYMaximumValue: validatedAxisYMaximumValue.value,
            axisYValues: {
                fixedValues: validatedAxisYValues.fixedValues,
                cyclicValue: validatedAxisYValues.cyclicValue
            },
            barsWidth: validatedBarsWidth.value,
            backgroundColor: validatedBackgroundColor.value,
            gridLineColor: validatedGridLineColor.value,
            border: validatedBorder.value,
            answers: validatedAxisXData.answers,
            answersBeyondAxisRange: validatedAxisXData.answersBeyondAxisRange,
            columnsColors: validatedAxisXData.colors,
            columnsDescriptions: validatedAxisXData.descriptions,
            columnsDescriptionsImages: validatedAxisXData.descriptionsImages,
            columnsNumber: validatedAxisXData.columnsNumber,
            axisXDescription: model["X axis description"],
            axisYDescription: model["Y axis description"],
            isNotActivity: ModelValidationUtils.validateBoolean(model["isNotActivity"]),
            exampleColumns: validatedAxisXData.examples
        };
    };

    presenter.validateAxisYMaximumValue = function (model) {
        var axisYMax = model["Y axis maximum value"].trim();

        if (ModelValidationUtils.isStringEmpty(axisYMax)) {
            return presenter.getErrorObject("YAM_02");
        }

        var wasFloat = false;
        var parsedAxisYMax = parseFloat(axisYMax);

        if (presenter.isFloat(parsedAxisYMax)) {
            wasFloat = true;
        }

        if(isNaN(parsedAxisYMax)) {
            return presenter.getErrorObject("YAM_02");
        }

        if (parsedAxisYMax &lt;= 0) {
            return presenter.getErrorObject("YAM_01");
        }

        return {isValid: true, value: parseInt(parsedAxisYMax), wasFloat: wasFloat};
    };

    function parseCyclicValue(axisYValues, axisYMaxValue) {
        var parsedValue = parseFloat(axisYValues);

        if (isNaN(parsedValue)) {
            return presenter.getErrorObject("YAV_01");
        }

        if (parsedValue &lt; 1) {
            return presenter.getErrorObject("YAV_02");
        }

        if (parsedValue &gt; axisYMaxValue) {
            return presenter.getErrorObject("YAV_04");
        }

        return {isValid: true, cyclicValue: [parseInt(parsedValue)], wasFloat: presenter.isFloat(parsedValue)};
    }

    function isValueNumber (value) {
        return !isNaN(value);
    }

    function isValuePositive(value) {
        return value &gt;= 0;
    }

    presenter.isFloat = function (value) {
        return Boolean(value % 1);
    };

    presenter.isFloatInValues = function (valuesArray) {
        return valuesArray.filter(presenter.isFloat).length &gt; 0;
    };

    presenter.isRGB = function (value) {
        if (value.charAt(0) != "#") {
            return false;
        }

        if (value.length != 7) {
            return false;
        }

        return value.slice(1, 7).split("").every(function (element) {
            return ("abcdefABCDEF0123456789".indexOf(element) != -1);
        })
    };

    function isInAxisRange(value) {
        //function for map, requires axisYMax passed to thisArg
        return value &lt;= this;
    }

    function parseValueToInt (value) {
        return parseInt(value);
    }

    function parseValueToBoolean (value) {
        return value != null &amp;&amp; value.toLowerCase() == 'true';
    }

    function parseFixedValues(axisYValues, axisYMaxValue) {

        var parsedValues = axisYValues.split(";").map(function (element) {
            return element.trim();
        }).map(function (element) {
            return parseFloat(element);
        });

        if (!parsedValues.every(isValueNumber)) {
            return presenter.getErrorObject("YAV_01");
        }

        if (!parsedValues.every(isValuePositive)) {
            return presenter.getErrorObject("YAV_03");
        }

        if (!parsedValues.every(isInAxisRange, axisYMaxValue)) {
            return presenter.getErrorObject("YAV_04");
        }

        var parsedIntValues = parsedValues.map(parseValueToInt);

        return {isValid: true, fixedValues: parsedIntValues, wasFloat: presenter.isFloatInValues(parsedValues)};
    }

    presenter.validateAxisYValues = function (model, axisYMaxValue) {
        var axisYValues = model["Y axis values"].trim();

        if (ModelValidationUtils.isStringEmpty(axisYValues)) {
            return {isValid: true, cyclicValue: [1], wasFloat: false};
        }

        if (axisYValues.charAt(axisYValues.length - 1) == "*") {
            return parseCyclicValue(axisYValues, axisYMaxValue);
        } else {
            return parseFixedValues(axisYValues, axisYMaxValue);
        }
    };

    presenter.validateBarsWidth = function (model) {
        var barsWidth = model["Bars width"].trim();

        if (ModelValidationUtils.isStringEmpty(barsWidth)) {
            return {isValid: true, value: 1};
        }

        var parsedBarsWidth = parseFloat(barsWidth);

        if (isNaN(parsedBarsWidth)) {
            return presenter.getErrorObject("BW_02");
        }

        if (parsedBarsWidth &lt; 0) {
            return presenter.getErrorObject("BW_01");
        }

        if (parsedBarsWidth &gt;= 0 &amp;&amp; parsedBarsWidth &lt;= 1) {
            return {isValid: true, value: 1};
        }

        return {isValid: true, value: parsedBarsWidth};
    };

    function validateRGBColor (color, errorCode) {
        if (color.charAt(0) == "#") {
            if (!presenter.isRGB(color)) {
                return presenter.getErrorObject(errorCode);
            }
        }

        return {isValid: true, value: color}
    }

    presenter.validateBackgroundColor = function (model) {
        var backgroundColor = model["Background color"].trim();
        var validatedRGB = validateRGBColor(backgroundColor, "BC_01");

        return setDefaultColorToValidatedRGB(validatedRGB, "#F5F5DC");
    };

    presenter.validateGridLineColor = function (model) {
        var gridLineColor = model["Grid line color"].trim();
        var validatedRGB = validateRGBColor(gridLineColor, "GLC_01");

        return setDefaultColorToValidatedRGB(validatedRGB, "black");
    };

    function setDefaultColorToValidatedRGB (validatedRGB, color) {
        if (validatedRGB.isValid) {
            if (ModelValidationUtils.isStringEmpty(validatedRGB.value)) {
                validatedRGB.value = color;
            }
        }

        return validatedRGB;
    }

    presenter.validateBorder = function (model) {
        var parsedBorder = parseFloat(model["Border"].trim());

        if (isNaN(parsedBorder)) {
            return {isValid: true, value: 0};
        }

        if (parsedBorder &lt; 0) {
            return {isValid: true, value: 0};
        }

        return {isValid: true, value: parsedBorder};
    };


    function getAttributeValueFromObject (element) {
        //function for map, requires passed object to thisArg {"attribute": "attributeString"}
        return element[this.attribute];
    }

    function isEmptyStringInValues (valuesArray) {
        var emptyStringsArray = valuesArray.map(ModelValidationUtils.isStringEmpty).filter(function (element) {
            return element;
        });

        return (emptyStringsArray.length &gt; 0)
    }

    presenter.validateAxisXData = function (model, axisYMaximumValue) {
        var axisXData = model["X axis data"];

        var answers = axisXData.map(getAttributeValueFromObject, {attribute: "Answer"});
        var colors = axisXData.map(getAttributeValueFromObject, {attribute: "Color"});
        var descriptions = axisXData.map(getAttributeValueFromObject, {attribute: "Description"});
        var descriptionsImages = axisXData.map(getAttributeValueFromObject, {attribute: "Description image"});
        var examples = axisXData.map(getAttributeValueFromObject, {attribute: "Example"});

        if (isEmptyStringInValues(answers)) {
            return presenter.getErrorObject("AXD_01");
        }

        var parsedAnswers = answers.map(function (element) {
            return Number(element);
        });

        if (!parsedAnswers.every(isValueNumber)) {
            return presenter.getErrorObject("AXD_02");
        }

        var wasFloat = presenter.isFloatInValues(parsedAnswers);
        var areValuesInScope = parsedAnswers.every(isValuePositive) &amp;&amp; parsedAnswers.every(isInAxisRange, axisYMaximumValue);

        var answersParsedToInt = parsedAnswers.map(parseValueToInt);
        var examplesParsedToBool = examples.map(parseValueToBoolean);

        return {
            isValid: true,
            wasFloat: wasFloat,
            answersBeyondAxisRange: !areValuesInScope,
            answers: answersParsedToInt,
            colors: colors,
            descriptions: descriptions,
            descriptionsImages: descriptionsImages,
            columnsNumber: answersParsedToInt.length,
            examples: examplesParsedToBool
        };
    };

    function deleteCommands () {
        delete presenter.getMaxScore;
        delete presenter.getScore;
        delete presenter.setState;
        delete presenter.getState;
        delete presenter.setWorkMode;
        delete presenter.setShowErrorsMode;
        delete presenter.reset;
    }

    presenter.upgradeModel = function(model) {
        return presenter.upgradeExample(model);
    }

    presenter.upgradeExample = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['X axis data'] === undefined) {
            upgradedModel['X axis data'] = [];
        }

        for (var i = 0; i &lt; upgradedModel['X axis data'].length; i++) {
            var axisXData = upgradedModel['X axis data'][i];
            if (axisXData['Example'] === undefined) {
                axisXData['Example'] = 'False';
            }
        }

        return upgradedModel;
    }

    presenter.runLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        if (!presenter.configuration.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[presenter.configuration.errorCode]);
            deleteCommands();
            return;
        }

        presenter.$view.find("div").attr('id', presenter.configuration.ID);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);


        presenter.observer = new presenter.graphObserver();

        if(!isPreview) {
            initializeGraphFromConfiguration();
        } else {
            initializeGraphFromConfiguration();
            presenter.graph.block();
        }
    };

    function initializeGraphFromConfiguration() {
        presenter.graph = new presenter.graphObject(
            presenter.$view,
            presenter.configuration.axisYMaximumValue,
            presenter.configuration.answers,
            presenter.configuration.columnsColors,
            presenter.configuration.columnsDescriptions,
            presenter.configuration.columnsDescriptionsImages,
            presenter.configuration.barsWidth,
            presenter.configuration.backgroundColor,
            presenter.configuration.gridLineColor,
            presenter.configuration.axisYValues.cyclicValue,
            presenter.configuration.axisYValues.fixedValues,
            30,
            presenter.configuration.border,
            presenter.configuration.axisYDescription,
            presenter.configuration.axisXDescription,
            presenter.configuration.exampleColumns
        );

        presenter.graph.initializeGraph();
    }

    presenter.setPlayerController = function (controller) {
        presenter.eventBus = controller.getEventBus();
    };

    presenter.sendEvent = function (isCorrect, column, change) {
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': (column + 1) + (change === "noChange" ? "" : " " + change),
            'value': '',
            'score': isCorrect ? '1' : '0'
        });
    };

    presenter.sendEventAllOk = function () {
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': 'all',
            'value': '',
            'score': ''
        });
    };

    presenter.isAllOk = function () {
        return (presenter.getScore() == presenter.getMaxScore());
    };

    presenter.onEventReceived = function (eventName, data) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }

        if (eventName == "GradualShowAnswers" &amp;&amp; data["moduleID"] === presenter.configuration.ID) {
            if (!isNaN(data["item"])) {
                var item = Number(data["item"]);
                presenter.gradualShowAnswers(item);
            }
        }

        if (eventName == "GradualHideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.getActivitiesCount = function () {
        return presenter.graph.getActivitiesCount();
    }

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'getValue': presenter.getValue,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.showAnswers = function () {
        if (presenter.configuration.isNotActivity) return;

        if (presenter.errorMode) {
            presenter.graph.setWorkMode();
            presenter.errorMode = false;
        }

        presenter.graph.block();
        presenter.graph.showAnswers();
        presenter.isShowAnswersActive = true;
    };

    presenter.gradualShowAnswers = function(item) {
        if (presenter.configuration.isNotActivity) return;

        if (presenter.errorMode) {
            presenter.graph.setWorkMode();
            presenter.errorMode = false;
        }

        presenter.graph.block();
        presenter.graph.gradualShowAnswers(item);
        presenter.isShowAnswersActive = true;
    }

    presenter.hideAnswers = function () {
        if (presenter.configuration.isNotActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.graph.unblock();
        presenter.graph.hideAnswers();
        presenter.isShowAnswersActive = false;
    };

    presenter.reset = function () {
        presenter.graph.setWorkMode();
        presenter.graph.reset();
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.graph.unblock();
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isNotActivity) return 0;

        return presenter.graph.getMaxScore();
    };

    presenter.getScore = function () {
        if (presenter.configuration.isNotActivity) return 0;

        return presenter.graph.getScore();
    };

    presenter.graphObject.prototype.isAttempted = function () {
        return !this._columns.every(function(column) {
            return column.getState() == 0;
        });
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isNotActivity) return 0;

        return presenter.graph.getErrorCount();
    };

    presenter.setShowErrorsMode = function () {
        if (presenter.configuration.isNotActivity) return;

        if(presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.graph.block();
        presenter.graph.setShowErrorsMode();
        presenter.errorMode = true;

        return false;
    };

    presenter.setWorkMode = function () {
        if (presenter.configuration.isNotActivity) return;

        if(presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.graph.unblock();
        presenter.graph.setWorkMode();
        presenter.errorMode = false;

        return false;
    };

    presenter.getState = function () {
        if(presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var selected = presenter.graph.getState();

        return JSON.stringify({
            selected: selected,
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (stateString) {
        if(!stateString) return;

        var state = JSON.parse(stateString);

        if(presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isVisible = state.isVisible;
        presenter.graph.setState(state.selected);
        presenter.setVisibility(state.isVisible);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Cross_Lesson" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Text" nameLabel="Cross_Lesson_property_text" type="string"/>
		<property name="Image" nameLabel="Cross_Lesson_property_image" type="image"/>
		<property name="LessonID" nameLabel="Cross_Lesson_property_lesson_id" type="string"/>
		<property name="CourseID" nameLabel="Cross_Lesson_property_course_id" type="string"/>
		<property name="Page" nameLabel="Cross_Lesson_property_Page" type="string"/>
		<property name="Type" nameLabel="Cross_Lesson_property_type" type="{lesson, ebook, course}"/>
		<property name="OpenLessonInCurrentTab" nameLabel="Cross_Lesson_property_open_in_current_tab" type="boolean"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Cross_Lesson_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Cross_Lesson_property_speech_texts" type="staticlist">
            <property name="GoToLesson" nameLabel="Cross_Lesson_property_go_to_lesson" type="staticrow">
                <property name="GoToLesson" nameLabel="Cross_Lesson_property_go_to_lesson" type="string"/>
            </property>
        </property>
		<property displayName="Check for access" name="CheckForAccess" nameLabel="Cross_Lesson_property_check_for_access" type="boolean"/>
		<property displayName="List of IDs required for access" name="AccessIDs" nameLabel="Cross_Lesson_property_access_ids" type="string"/>
	</model>
<css>.cross-lesson-wrapper {
    border: solid 0px black;
    padding: 0;
    margin: 0;
    display: block;
}

.cross-lesson-text,
.cross-lesson-image {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    cursor: pointer;
    position: absolute;
}
</css><view>&lt;div class="cross-lesson-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="cross-lesson-wrapper"&gt;

&lt;/div&gt;
</preview><presenter>function AddonCross_Lesson_create(){
    var presenter = function() {};

    var crossLessonEventType = "crossLesson";
    var crossLessonUserAccessEventType = "crossLessonUserAccess";

    var crossLessonEventReceivedType = "crossLessonHasUserAccess:";

    const errorCodes = {
        "V_01": "Lesson ID is missing",
        "V_02": "Course ID is invalid",
        "V_03": "Type is invalid. Lesson type should be either 'lesson', 'ebook' or 'course'.",
        "V_04": "Access ids given but CheckAccess is not selected.",
        "V_05": "Incorrect values provided in AccessIDs field."
    };

    const resourceTypes = {
        lesson: "lesson",
        ebook: "ebook",
        course: "course"
    };

    presenter.keyboardControllerObject = null;

    presenter.DEFAULT_TTS_PHRASES = {
        GO_TO_LESSON: "Go to lesson"
    };

    presenter.uniqueIdentifier = null;

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    function presenterLogic(view, model, preview) {
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        if (presenter.configuration.isError) {
            presenter.createErrorView(view, presenter.configuration.errorCode);
            return;
        }
        presenter.accessCacheId = presenter.generateAccessCacheId();
        presenter.createView(view);
        presenter.connectHandlers();
        presenter.setSpeechTexts(upgradedModel["speechTexts"]);
        presenter.buildKeyboardController();
        presenter.setUniqueIdentifier(model.ID);
        presenter.handleUserAccess();
    };

    presenter.validateModel = function (model) {
        var validatedType = presenter.validateType(model['Type']);
        if (!validatedType.isValid) {
            return {isError: true, errorCode: 'V_03'};
        }
        if (
            validatedType.value != resourceTypes.course &amp;&amp;
            ModelValidationUtils.isStringEmpty(model["LessonID"])
          ) {
            return {isError: true, errorCode: 'V_01'};
        }
        var validatedCourseId = presenter.validateId(model['CourseID'], false, validatedType.value);
        if (!validatedCourseId.isValid) {
            return {isError: true, errorCode: 'V_02'};
        }

        var checkForAccess = ModelValidationUtils.validateBoolean(model.CheckForAccess);

        if (!checkForAccess &amp;&amp; model.AccessIDs.trim().length &gt; 0) {
            return {isError: true, errorCode: 'V_04'};
        }

        var checkForLessonIdAccess = false;
        if (checkForAccess &amp;&amp; model.AccessIDs.trim().length === 0) {
            model.AccessIDs = validatedCourseId.value
                ? validatedCourseId.value
                : model.LessonID;
            checkForLessonIdAccess = model.AccessIDs == model.LessonID;
        }

        var validatedAccessIds = presenter.validateAccessIds(model.AccessIDs, checkForLessonIdAccess);
        if (checkForAccess &amp;&amp; !validatedAccessIds.isValid) {
            return {isError: true, errorCode: 'V_05'};
        }

        return {
            isError: false,
            buttonText: model['Text'],
            lessonID: model['LessonID'],
            courseID: validatedCourseId.value,
            type: validatedType.value,
            page: model['Page'],
            image: model['Image'],
            openLessonInCurrentTab: ModelValidationUtils.validateBoolean(model.OpenLessonInCurrentTab),
            langTag: model['langAttribute'],
            checkForAccess: checkForAccess,
            accessIds: validatedAccessIds.value
        }
    };

    presenter.validateId = function(id, isRequired, type) {
        var idReg = /^\d*$/;
        var isValid = false;

        if (id === "") {
            if (type != resourceTypes.course) {
                isValid = !isRequired;
            } else {
                return {
                    isValid: isValid
                }
            }
        } else {
            isValid = idReg.test(id.trim());
        }

        return {
            isValid: isValid,
            value: isValid ? id.trim() : NaN
        }
    };

    presenter.validateType = function(type) {
        if (!type) {
            return {
                isValid: true,
                value: resourceTypes.lesson
            };
        }
        if (type == resourceTypes.lesson || type == resourceTypes.ebook || type == resourceTypes.course) {
            return {
                isValid: true,
                value: type
            };
        } else {
            return {
                isValid: false,
                value: NaN
            };
        }
    };

    presenter.validateAccessIds = function AddonCross_Lesson_validateAccessIds (raw, isLessonId) {
        const ids = presenter.transformAccessIdsToArray(raw);
        const onlyDigitsRegex = /^\d*$/;

        const filteredIds = ids
            .map(id =&gt; id.trim())
            .filter((id) =&gt; (id.length &gt; 0 &amp;&amp; (onlyDigitsRegex.test(id) || onlyDigitsRegex.test(isLessonId))));

        if (ids.length !== filteredIds.length) {
            return {isValid: false};
        }

        return {isValid: true, value: filteredIds};
    };

    presenter.transformAccessIdsToArray = function (raw) {
        let ids = String(raw).split(",");
        if (ids.length === 1 &amp;&amp; ids[0] === "") {
            ids = [];
        }
        return ids;
    };

    presenter.createView = function (view) {
        presenter.$view = $(view);
        presenter.$wrapper = presenter.$view.find('.cross-lesson-wrapper');
        if (presenter.configuration.image) {
            presenter.createImageElement(presenter.$wrapper);
        }
        presenter.createTextElement(presenter.$wrapper);
    };

    presenter.createTextElement = function ($element) {
        var textElement = document.createElement('div');
        $(textElement).addClass('cross-lesson-text');
        $(textElement).text(presenter.configuration.buttonText);
        $element.append(textElement);
    };

    presenter.createImageElement = function ($element) {
        var imageElement = document.createElement('img');
        $(imageElement).addClass('cross-lesson-image');
        $(imageElement).attr('src', presenter.configuration.image);
        $element.append(imageElement);
    };

    presenter.createErrorView = function (view, errorCode) {
        presenter.$view = $(view);
        presenter.$view.html(errorCodes[errorCode]);
    };

    presenter.connectHandlers = function () {
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            presenter.$wrapper[0].addEventListener('touchend', presenter.clickHandler);
        } else {
            presenter.$wrapper[0].addEventListener('click', presenter.clickHandler);
        }
    };

    presenter.clickHandler = function (event) {
        event.preventDefault();
        event.stopPropagation();
        presenter.requestCrossLesson();
    };

    presenter.onExternalMessage = function AddonCross_Lesson_onExternalMessage (event) {
        const data = event.data;
        if (typeof data === 'string' &amp;&amp; data.indexOf(crossLessonEventReceivedType) !== -1 &amp;&amp; data.indexOf(presenter.uniqueIdentifier) !== -1) {
            const lenToSlice = crossLessonEventReceivedType.length;
            const lenOfWordTrue = 4;
            const value = data.slice(lenToSlice, lenToSlice + lenOfWordTrue);
            if (value !== 'true') {
                presenter.hide();
            }
            presenter.updateAccessCache(value == 'true');
        }
    };

    presenter.updateAccessCache = function(hasAccess) {
        if (presenter.configuration.accessIds.length == 0) return;
        if (window.crossLessonAccessCache == null) window.crossLessonAccessCache = {};
        window.crossLessonAccessCache[presenter.accessCacheId] = hasAccess;
    }

    presenter.generateAccessCacheId = function() {
        if (presenter.configuration.accessIds.length == 0) return '';
        var sortedIds = presenter.configuration.accessIds.slice().sort();
        return sortedIds.toString();
    }

    presenter.requestCrossLesson = function () {
       const data = presenter.getExternalEventData();
       if (!data) {
           return
       }

       presenter.playerController.sendExternalEvent(crossLessonEventType, data);
    };

    presenter.setUniqueIdentifier = function AddonCross_Lesson_setUniqueIdentifier (modelID){
        if (!presenter.playerController) {
            return;
        }

        const pageIndex = presenter.playerController.getCurrentPageIndex();
        const pageID = presenter.playerController.getPresentation().getPage(pageIndex).getId();
        presenter.uniqueIdentifier = `${modelID}-${pageID}`;

        if (!presenter.$view) {
            return;
        }

        if (presenter.$view.closest('.ic_header').length) {
            presenter.uniqueIdentifier += "-header";
        }

        if (presenter.$view.closest('.ic_footer').length) {
            presenter.uniqueIdentifier += "-footer";
        }
    };

    presenter.handleUserAccess = function AddonCross_Lesson_handleUserAccess () {
        if (!presenter.playerController) {
            return;
        }

        if (!presenter.configuration.checkForAccess) {
            return;
        }

        if (window.crossLessonAccessCache != null) {
            if (window.crossLessonAccessCache[presenter.accessCacheId] !== undefined) {
                if (!window.crossLessonAccessCache[presenter.accessCacheId]) {
                    presenter.hide();
                }
                return;
            }
        }

        const data = {
            "coursesIds": presenter.configuration.accessIds,
            "uniqueId": presenter.uniqueIdentifier
        };

        window.addEventListener("message", presenter.onExternalMessage);
        presenter.playerController.sendExternalEvent(crossLessonUserAccessEventType, JSON.stringify(data));
    };

    presenter.getExternalEventData = function AddonCross_Lesson_getExternalEventData () {
        if (!presenter.playerController) {
            console.error("Cannot make a request: no player controller");
            return;
        }

        var data = {
            type: presenter.configuration.type,
            openLessonInCurrentTab: presenter.configuration.openLessonInCurrentTab
        };
        if (presenter.configuration.lessonID) {
            data.lessonID = presenter.configuration.lessonID;
        }
        if (presenter.configuration.page) {
            data.page = presenter.configuration.page;
        }
        if (presenter.configuration.courseID) {
            data.courseID = presenter.configuration.courseID;
        }

        return JSON.stringify(data);
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function (isVisible) {
        if (isVisible) {
            presenter.$view.css('display', '');
            presenter.$view.css('visibility', 'visible');
        } else {
            presenter.$view.css('display', 'none');
            presenter.$view.css('visibility', 'hidden');
        }
    };

    presenter.executeCommand = function(name) {
        var commands = {
            'requestCrossLesson': presenter.requestCrossLesson,
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, [], presenter);
    };

    presenter.upgradeOpenLessonInCurrentTab = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["OpenLessonInCurrentTab"]) {
            upgradedModel["OpenLessonInCurrentTab"] = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function AddonCross_Lesson_upgradeSpeechTexts (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["GoToLesson"]) {
            upgradedModel["speechTexts"]["GoToLesson"]
              = {GoToLesson: ""};
        }

        return upgradedModel;
    };

    presenter.upgradeCheckForAccess = function AddonCross_Lesson_upgradeCheckForAccess (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (upgradedModel['CheckForAccess'] === undefined) {
            upgradedModel['CheckForAccess'] =  "False";
        }

        return upgradedModel;
    };

    presenter.upgradeAccessIds = function AddonCross_Lesson_upgradeAccessIds (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (upgradedModel['AccessIDs'] === undefined) {
            upgradedModel['AccessIDs'] =  "";
        }

        return upgradedModel;
    };

    presenter.upgradeModel = function AddonCross_Lesson_upgradeModel (model) {
        var upgradedModel = presenter.upgradeOpenLessonInCurrentTab(model);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        upgradedModel = presenter.upgradeCheckForAccess(upgradedModel);
        upgradedModel = presenter.upgradeAccessIds(upgradedModel);
        return upgradedModel;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            GoToLesson: presenter.DEFAULT_TTS_PHRASES.GO_TO_LESSON
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            GoToLesson: TTSUtils.getSpeechTextProperty(
                speechTexts.GoToLesson.GoToLesson,
                presenter.speechTexts.GoToLesson)
        };
    };

    function CrossLessonKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    CrossLessonKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    CrossLessonKeyboardController.prototype.constructor = CrossLessonKeyboardController;

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject = new CrossLessonKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return this.$view.find(".cross-lesson-wrapper");
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event)
    };

    CrossLessonKeyboardController.prototype.selectAction = function () {
        this.getTarget(this.keyboardNavigationCurrentElement, true)[0].click();
    };

    CrossLessonKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    CrossLessonKeyboardController.prototype.mark = function (element) {
        return;
    };

    CrossLessonKeyboardController.prototype.unmark = function (element) {
        return;
    };

    CrossLessonKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        this.readCurrentElement();
    };

    CrossLessonKeyboardController.prototype.readCurrentElement = function () {
        var text = this.getTarget(this.keyboardNavigationCurrentElement, false)[0].innerText;
        if (text) {
            text = [TTSUtils.getTextVoiceObject(text, presenter.configuration.langTag)];
        } else {
            text = presenter.speechTexts.GoToLesson;
        }
        presenter.speak(text);
    };

    CrossLessonKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    presenter.speak = function Assessments_Navigation_Bar_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function Assessments_Navigation_Bar_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Custom_Scoring" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Script" nameLabel="Custom_Scoring_property_script" type="text"/>
		<property name="Max Score" nameLabel="Custom_Scoring_property_max_score" type="string"/>
	</model>
<css/><view/><preview/><presenter>function AddonCustom_Scoring_create(){
    var presenter = function(){};

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model);
    };

    presenter.ERROR_CODES = {
        'ERR_02': "Invalid max score!"
    };

    function removeAPIMethods() {
        delete presenter.getMaxScore;
        delete presenter.getScore;
        delete presenter.getErrorCount;
        delete presenter.executeCommand;
        delete presenter.evaluate;
        delete presenter.setShowErrorsMode;
        delete presenter.setScore;
        delete presenter.setScoreCommand;
        delete presenter.setErrors;
        delete presenter.setErrorsCommand;
    }

    presenter.presenterLogic = function (view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.configuration = presenter.parseModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);

            removeAPIMethods();
        }
    };

    presenter.parseModel = function (model) {
        var modelMaxScore = model['Max Score'],
            maxScore = 0;

        if (!ModelValidationUtils.isStringEmpty(modelMaxScore)) {
            var validatedMaxScore = ModelValidationUtils.validatePositiveInteger(modelMaxScore);

            if (!validatedMaxScore.isValid) {
                return { isValid: false, errorCode: 'ERR_02'}
            }

            maxScore = validatedMaxScore.value;
        }

        return {
            isValid: true,
            addonID: model.ID,
            script: model['Script'],
            scoring: {
                maxScore: maxScore,
                score: 0,
                errors: 0
            }
        };
    };

    presenter.setScore = function (score) {
        var validatedScore = ModelValidationUtils.validateIntegerInRange(score, presenter.configuration.scoring.maxScore);

        if (validatedScore.isValid) {
            presenter.configuration.scoring.score = validatedScore.value;
        } else {
            presenter.configuration.scoring.score = 0;
        }
    };

    presenter.setErrors = function (errors) {
        var validatedErrors = ModelValidationUtils.validatePositiveInteger(errors);

        if (validatedErrors.isValid) {
            presenter.configuration.scoring.errors = validatedErrors.value;
        } else {
            presenter.configuration.scoring.errors = 0;
        }
    };

    presenter.getMaxScore = function () {
        return presenter.configuration.scoring.maxScore;
    };

    presenter.getErrorCount = function () {
        presenter.evaluateScript();

        return presenter.configuration.scoring.errors;
    };

    presenter.getScore = function () {
        presenter.evaluateScript();

        return presenter.configuration.scoring.score;
    };

    presenter.evaluateScript = function () {
        if (!presenter.configuration.script) {
            return;
        }

        try {
            eval(presenter.configuration.script);
        } catch (error) {
            Helpers.alertErrorMessage(error, "Custom Score - problem occurred while running scripts!");
        }
    };

    presenter.evaluate = function () {
        presenter.evaluateScript();

        return {
            score: presenter.configuration.scoring.score,
            errors: presenter.configuration.scoring.errors
        };
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'evaluate': presenter.evaluate,
            'setScore': presenter.setScoreCommand,
            'setErrors': presenter.setErrorsCommand
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setShowErrorsMode = function () {
        presenter.evaluateScript();
    };

    presenter.setScoreCommand = function (params) {
        presenter.setScore(params[0]);
    };

    presenter.setErrorsCommand = function (params) {
        presenter.setErrors(params[0]);
    };

    presenter.getState = function () {
        return JSON.stringify({
            score: presenter.configuration.scoring.score,
            errors: presenter.configuration.scoring.errors
        });
    };

    presenter.setState = function (state) {
        if (!state) return;

        var parsedState = JSON.parse(state);

        presenter.configuration.scoring.score = parsedState.score;
        presenter.configuration.scoring.errors = parsedState.errors;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Dice" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="elementsList" nameLabel="Dice_property_elements_list" type="list">
            <property name="name" nameLabel="Dice_property_name" type="string"/>
            <property name="image" nameLabel="Dice_property_image" type="image"/>
        </property>
        <property name="animationLength" nameLabel="Dice_property_animation_length" type="string"/>
        <property name="isDisabled" nameLabel="Dice_property_is_disabled" type="boolean"/>
        <property name="initialItem" nameLabel="Dice_property_initial_item" type="string"/>
        <property name="worksWith" nameLabel="Dice_property_works_with" type="string"/>
    </model>
<css>.addon-Dice-wrapper{
    width: 100%;
    height: 100%;
}

.addon-Dice-image-element {
    width: 100%;
    height: 100%;
    background-repeat: no-repeat;
    background-size: contain;
    opacity: 0;
    position: absolute;
}

.dice-element-visible {
    opacity: 1;
}

.addon-Dice-wrapper .loading {
    background-color: gray;
    opacity: 0.8;
    width: 100%;
    height: 100%;
}

.addon-Dice-dice-container {
    width: 100%;
    height: 100%;
    position: absolute;
}
</css><view>&lt;div class="addon-Dice-wrapper"&gt;
    &lt;div class="addon-Dice-dice-container"&gt;&lt;/div&gt;
    &lt;div class="loading"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="addon-Dice-wrapper"&gt;
    &lt;div class="addon-Dice-dice-container"&gt;&lt;/div&gt;
    &lt;div class="loading"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonDice_create() {
    var presenter = function (){};

    presenter.ERROR_CODES = {
        "animationLength_INT01": "Animation length can't be empty",
        "animationLength_INT02": "Animation length is not a valid integer",
        "animationLength_INT04": "Animation length must be positive",
        "initialItem_INT02": "Value of initial item is not a valid integer",
        "initialItem_INT04": "Value of initial item is not positive integer",
        "initialItem_INI01": "Initial item can't be bigger than elements count",
        "elementsList|number_INT02": "Element value in list is not valid integer"

    };

    presenter.configuration = {
        isDisabled: false,
        "Is Visible": false,
        animationLength: 0,
        initialItem: null,
        elementsList: []
    };

    presenter.state = {
        images: [],
        loadedImages: 0,
        isLoaded: false,
        elements: [],
        wrapperElement: null,
        loadingElement: null,
        isDisabled: false,
        isRolling: false,
        timeoutsHandlers: [],
        view: null,
        rolledElement: -1,
        isVisible: false,
        disabledByEvent: false,
        worksWith: null,
    };

    var CLASSES =  {
        visibleElement: "dice-element-visible",
        imageElement: "addon-Dice-image-element"
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model);
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);

        presenter.diceKeyboardController = new DiceKeyboardController();
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model);
    };

    presenter.validateModel = function (model) {
        var modelValidator = new ModelValidator();

        var validatedModel = modelValidator.validate(model, [
            ModelValidators.Boolean("isDisabled"),
            ModelValidators.Integer("animationLength", {minValue: 0}),
            ModelValidators.Integer("initialItem", {minValue: 1, default: null}),
            ModelValidators.List("elementsList", [
                ModelValidators.String("name", {default: null}),
                ModelValidators.String("image", {trim: true, default: null})
            ]),
            ModelValidators.Boolean("Is Visible"),
            ModelValidators.String("worksWith", {default: null}),
            ModelValidators.DumbString("ID")
        ]);

        if (!validatedModel.isValid) {
            return validatedModel;
        }

        if (validatedModel.value.initialItem &gt; validatedModel.value.elementsList.length) {
            return {
                isValid: false,
                fieldName: ["initialItem"],
                errorCode: "INI01"
            }
        }

        if (validatedModel.value.initialItem !== null) {
            validatedModel.value.initialItem -= 1;
        }

        return validatedModel;
    };

    presenter.enableAddon = function () {
        presenter.state.isLoaded = true;
        presenter.state.loadingElement.style.display = 'none';
    };

    presenter.enableIfIsLoaded = function () {
        if (presenter.state.images.length === presenter.state.loadedImages) {
            presenter.enableAddon();
        }
    };

    presenter.loadedImage = function () {
        presenter.state.loadedImages += 1;
        presenter.enableIfIsLoaded();
    };

    presenter.loadImages = function () {
        presenter.configuration.elementsList.forEach(function (element) {
            var imgElement;
            if (element.image !== null) {
                imgElement = new Image();
                presenter.state.images.push(imgElement);
                imgElement.onload = presenter.loadedImage;
                imgElement.onerror = presenter.loadedImage;

                imgElement.src = element.image;
            }
        });
    };

    presenter.buildElement = function (name, background) {
        var element = document.createElement('div');
        if (name !== null) {
            var textNode = document.createTextNode(name);
            element.appendChild(textNode);
        }

        if (background !== null) {
            element.style.backgroundImage = "url(" + background + ")";
        }

        element.classList.add(CLASSES.imageElement);

        element.addEventListener('click', presenter.onDiceClick);

        return element;
    };

    presenter.buildElements = function () {
        presenter.configuration.elementsList.forEach(function (element, index) {
            var text = element.image === null &amp;&amp; element.name === null ? index + 1 : element.name;
            var elementDiv = presenter.buildElement(text, element.image);

            presenter.state.elements.push(elementDiv);
            presenter.state.wrapperElement.appendChild(elementDiv);
        });
    };

    presenter.randElement = function () {
        return parseInt(Math.random() * (presenter.state.elements.length) + "");
    };

    presenter.setElement = function (index) {
        presenter.state.elements.forEach(function (el) {
            el.classList.remove(CLASSES.visibleElement);
        });

        presenter.state.elements[index].classList.add(CLASSES.visibleElement);
    };

    presenter.setRandomElement = function () {
        var element = presenter.randElement();
        presenter.setElement(element);

        return element;
    };

    presenter.initializeStartItem = function () {
        var initItemIndex = presenter.configuration.initialItem;
        initItemIndex = initItemIndex === null ? presenter.randElement() : initItemIndex;

        presenter.state.rolledElement = initItemIndex;
        presenter.setElement(initItemIndex);
    };

    presenter.initialize = function (view, model)  {
        var validatedModel = presenter.validateModel(model);

        if (!validatedModel.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.fieldName.join("|") + "_" + validatedModel.errorCode);
            return;
        }

        presenter.configuration = validatedModel.value;

        presenter.state.wrapperElement = view.getElementsByClassName("addon-Dice-dice-container")[0];
        presenter.state.loadingElement = view.getElementsByClassName("loading")[0];
        presenter.state.isDisabled = presenter.configuration.isDisabled;
        presenter.state.view = view;
        presenter.state.isVisible = presenter.configuration['Is Visible'];

        presenter.buildElements();
        presenter.loadImages();
        presenter.enableIfIsLoaded();
        presenter.initializeStartItem();

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.state.view);
        MutationObserverService.setObserver();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.state.view) {
            return;
        }

        presenter.state.elements.forEach(function (element) {
            element.removeEventListener('click', presenter.onDiceClick);
        });

        presenter.state.timeoutsHandlers.forEach(function (handler) {
            clearTimeout(handler);
        });
    };

    presenter.callExternalAddon = function (distance) {
        if (presenter.configuration.worksWith !== null) {
            var module = presenter.playerController.getModule(presenter.configuration.worksWith);
            if (module) {
                module.diceExecute(distance + 1);
            }
        }
    };

    presenter.sendEventDiceRolled = function (value) {
        var elementText = presenter.configuration.elementsList[value - 1].name;
        presenter.eventBus.sendEvent('ValueChanged', {
            source: presenter.configuration.ID,
            item:  value + '',
            value : (elementText || value) + ''
        });
    };

    presenter.onDiceRoll = function (isLast) {
        var element = presenter.setRandomElement();

        if (isLast) {
            presenter.state.rolledElement = element;
            presenter.state.isRolling = false;

            presenter.state.elements.forEach(function (element) {
                element.classList.remove('isRolling');
            });

            presenter.sendEventDiceRolled(element + 1);
            presenter.callExternalAddon(element);
        }
    };

    presenter.executeRoll = function () {
        presenter.state.isRolling = true;
        presenter.state.timeoutsHandlers = [];

        var animationLength = presenter.configuration.animationLength,
            throwCount = parseInt((Math.random() * 10) + 5 + ""),
            acceleration = 20,
            jump = acceleration * animationLength * animationLength / throwCount,
            timeoutsCallbacks = [],
            i;

        for (i = 1; i &lt;= throwCount; i++) {
            var time = Math.sqrt(i * jump / acceleration);

            timeoutsCallbacks.push({
                callback: presenter.onDiceRoll.bind({}, i === 1),
                timeout: parseInt(animationLength - time)
            });
        }

        for (i = timeoutsCallbacks.length - 1; i &gt;= 0; i--) {
            var timeoutID = setTimeout(timeoutsCallbacks[i].callback, timeoutsCallbacks[i].timeout);
            presenter.state.timeoutsHandlers.push(timeoutID);
        }

        presenter.state.elements.forEach(function (element) {
            element.classList.add('isRolling');
        });
    };

    presenter.roll = function () {
        if (presenter.state.isDisabled) {
            return;
        }

        if (!presenter.state.isLoaded) {
            return;
        }

        if (presenter.state.isRolling) {
            return;
        }

        if (presenter.state.disabledByEvent) {
            return;
        }

        presenter.sendDiceRollStartEvent();
        presenter.executeRoll();
    };

    presenter.sendDiceRollStartEvent = function () {
        presenter.eventBus.sendEvent('ValueChanged', {
            source: presenter.configuration.ID,
            item: '',
            value : 'start'
        });
    };

    presenter.onDiceClick = function () {
        presenter.roll();
    };

    presenter.setVisibility = function (isVisible) {
        $(presenter.state.view).css('visibility', isVisible ? 'visible' : 'hidden');
        $(presenter.state.view).css('display', isVisible ? 'block' : 'none');

        presenter.state.isVisible = isVisible;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.reset = function () {
        presenter.setVisibility(presenter.configuration['Is Visible']);

        presenter.initializeStartItem();
        presenter.state.isDisabled = presenter.configuration.isDisabled;
        presenter.state.disabledByEvent = false;
        presenter.deselectDiceAsDisabled();
        presenter.state.timeoutsHandlers.forEach(function (id) {
            clearTimeout(id);
        });
        presenter.state.timeoutsHandlers = [];
        presenter.state.isRolling = false;
    };

    presenter.selectDiceAsDisabled = function () {
        presenter.state.wrapperElement.classList.add('disabled');
    };

    presenter.deselectDiceAsDisabled = function () {
        presenter.state.wrapperElement.classList.remove('disabled');
    };

    presenter.showAnswers = function () {
        presenter.selectDiceAsDisabled();
        presenter.state.disabledByEvent = true;
    };

    presenter.hideAnswers = function () {
        presenter.deselectDiceAsDisabled();
        presenter.state.disabledByEvent = false;
    };

    presenter.setWorkMode = function () {
        presenter.deselectDiceAsDisabled();
        presenter.state.disabledByEvent = false;
    };

    presenter.setShowErrorsMode = function () {
        presenter.selectDiceAsDisabled();
        presenter.state.disabledByEvent = true;
    };

    presenter.disable = function () {
        presenter.state.isDisabled = true;
        presenter.selectDiceAsDisabled();
    };

    presenter.enable = function () {
        presenter.state.isDisabled = false;
        presenter.deselectDiceAsDisabled();
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state);

        presenter.setVisibility(parsedState.isVisible);
        presenter.state.isDisabled = parsedState.isDisabled;
        presenter.state.rolledElement = parsedState.rolledElement;
        presenter.setElement(presenter.state.rolledElement);
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.state.isVisible,
            isDisabled: presenter.state.isDisabled,
            rolledElement: presenter.state.rolledElement
        });
    };

    presenter.getScore = function () {
        return 0;
    };

    presenter.getMaxScore = function () {
        return 0;
    };

    presenter.getErrorCount = function () {
        return 0;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'disable': presenter.disable,
            'enable': presenter.enable,
            'roll': presenter.roll
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    function DiceKeyboardController() {
        KeyboardController.call(this, $(presenter.state.view), 1);
    }

    DiceKeyboardController.prototype = Object.create(KeyboardController.prototype);
    DiceKeyboardController.prototype.constructor = DiceKeyboardController;

    DiceKeyboardController.prototype.selectAction = function () {
        presenter.roll();
    };

    presenter.keyboardController = function (keyCode, isShift, event) {
        presenter.diceKeyboardController.handle(keyCode, isShift, event);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Done" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property isLocalized="true" name="Text" nameLabel="Done_property_text" type="string"/>
	</model>
<css>.done-wrapper,
.done-wrapper .done-container,
.done-wrapper .done-container .done-button {
    width: 100%;
    height: 100%;
}

.done-wrapper .done-container .done-button {
    background: url('resources/done-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}

.done-wrapper.disabled .done-container .done-button {
    background: url('resources/done-button-disabled.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}
</css><view>&lt;div class="done-wrapper"&gt;
    &lt;div class="done-container"&gt;
        &lt;div class="done-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="done-wrapper"&gt;
    &lt;div class="done-container"&gt;
        &lt;div class="done-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonDone_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.wasSubmitted = false;
    presenter.isVisible = null;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('Submitted', this);
    };

    presenter.onEventReceived = function(eventName) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        } else if (eventName == 'Submitted') {
            presenter.pageSubmittedDeferred.resolve();
        }
    };

    presenter.createEventData = function (additionalEventData) {
        var eventData = { 'source': presenter.configuration.addonID };
        for (var key in additionalEventData) {
            eventData[key] = additionalEventData[key];
        }
        return eventData;
    };

    presenter.sendEvent = function(eventName, eventData) {
        presenter.eventBus.sendEvent(eventName, eventData);
    };

    presenter.ERROR_CODES = {

    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model, isPreview) {
        return {
            'buttonText' : model['Text'],
            'isVisible' : ModelValidationUtils.validateBoolean(model["Is Visible"]),
            'addonID' : model['ID']
        }
    };

    function getAllOfTheModulesThatImplementIsAttempted() {
        var pageIndex = presenter.playerController.getCurrentPageIndex(),
            ids = presenter.playerController.getPresentation().getPage(pageIndex).getModulesAsJS(),
            modules = [];

        $.each(ids, function() {
            var id = this.toString(),
                currentModule = presenter.playerController.getModule(id);

            if (currentModule &amp;&amp; currentModule.isAttempted !== undefined) {
                modules.push(currentModule);
            }
        });

        return modules;
    }

    function areAllModulesAttempted() {
        var areAllAttempted = true;

        $.each(presenter.modulesOnPage, function() {
            if (!this.isAttempted()) {
                areAllAttempted = false;
                return false; // break;
            }
        });

        return areAllAttempted;
    }

    function runLogic(view, model, isPreview) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();
        presenter.pageSubmittedDeferred = new $.Deferred();
        presenter.pageSubmitted = presenter.pageSubmittedDeferred.promise();
        presenter.runEndedDeferred = new $.Deferred();
        presenter.runEnded = presenter.runEndedDeferred.promise();

        presenter.configuration = presenter.validateModel(model, isPreview);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.$view = $(view);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        presenter.submitButton = presenter.$view.find('.done-button');
        presenter.submitButton.html(presenter.configuration.buttonText);
        presenter.doneWrapper = presenter.$view.find('.done-wrapper');
        presenter.doneWrapper.addClass('disabled');

        presenter.pageSubmitted.then(function() {
            presenter.doneWrapper.removeClass('disabled');
        });

        presenter.pageLoaded.then(function() {

            presenter.modulesOnPage = getAllOfTheModulesThatImplementIsAttempted();

            presenter.submitButton.click(function(e) {
                e.stopPropagation();

                if (presenter.doneWrapper.hasClass('disabled')) {
                    if (areAllModulesAttempted()) {
                        presenter.sendEvent('AllAttempted', presenter.createEventData());
                    } else {
                        presenter.sendEvent('NotAllAttempted', presenter.createEventData());
                    }
                } else {
                    presenter.wasSubmitted = true;
                    presenter.sendEvent('Done', presenter.createEventData());
                }

            });

            presenter.runEndedDeferred.resolve();
        });

    }

    presenter.run = function(view, model) {
        runLogic(view, model, false);
    };

    presenter.setShowErrorsMode = function() {};

    presenter.setWorkMode = function() {};

    presenter.reset = function() {
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.wasSubmitted = false;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            "show": presenter.show,
            "hide": presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getErrorCount = function() {
        return 0;
    };

    presenter.getMaxScore = function() {
        return 0;
    };

    presenter.getScore = function() {
        return 0;
    };

    presenter.getState = function() {
        return JSON.stringify({
            'wasSubmitted' : presenter.wasSubmitted,
            'isVisible' : presenter.isVisible
        });
    };

    presenter.setState = function(state) {
        presenter.wasSubmitted = JSON.parse(state).wasSubmitted;
        presenter.isVisible = JSON.parse(state).isVisible;

        presenter.runEnded.then(function() {
            if (presenter.wasSubmitted) { presenter.doneWrapper.removeClass('disabled'); }
            presenter.setVisibility(presenter.isVisible);
        });
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Double_State_Button" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="Text" nameLabel="Double_State_Button_property_text" type="string"/>
		<property name="Image" nameLabel="Double_State_Button_property_image" type="image"/>
		<property name="imageAlternativeText" nameLabel="Double_State_Button_property_image_alternative_text" type="string"/>
		<property isDefault="true" name="onSelected" nameLabel="Double_State_Button_property_on_selected" type="event"/>
		<property isLocalized="true" name="Text selected" nameLabel="Double_State_Button_property_text_selected" type="string"/>
		<property name="Image selected" nameLabel="Double_State_Button_property_image_selected" type="image"/>
		<property name="imageSelectedAlternativeText" nameLabel="Double_State_Button_property_image_selected_alternative_text" type="string"/>
		<property name="onDeselected" nameLabel="Double_State_Button_property_on_deselected" type="event"/>
		<property name="isSelected" nameLabel="Double_State_Button_property_is_selected" type="boolean"/>
		<property name="Disable" nameLabel="Double_State_Button_property_disable" type="boolean"/>
		<property name="Do not block in check mode" nameLabel="Double_State_Button_property_do_not_block_in_check_mode" type="boolean"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Double_state_button_property_lang_attribute" type="string"/>
		<property displayName="Render SVG as HTML" name="renderSVGAsHTML" nameLabel="Double_state_button_render_SVG_as_HTML" type="boolean"/>
		<property displayName="Omit text in TTS" name="omitTextInTTS" nameLabel="Double_state_button_omit_text_in_tts" type="boolean"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="Double_state_button_speech_texts" type="staticlist">
			<property name="selectButton" nameLabel="Double_state_button_speech_text_select" type="staticrow">
				<property name="selectButton" nameLabel="Double_state_button_speech_text_select" type="string"/>
			</property>
			<property name="deselectButton" nameLabel="Double_state_button_speech_text_deselect" type="staticrow">
				<property name="deselectButton" nameLabel="Double_state_button_speech_text_deselect" type="string"/>
			</property>
			<property name="speechTextDisabled" nameLabel="Double_state_button_speech_text_disabled" type="staticrow">
				<property name="speechTextDisabled" nameLabel="Double_state_button_speech_text_disabled" type="boolean"/>
			</property>
			<property name="disabledButton" nameLabel="Double_state_button_speech_text_disabled_button" type="staticrow">
				<property name="disabledButton" nameLabel="Double_state_button_speech_text_disabled_button" type="string"/>
			</property>
		</property>
	</model>
<css>.doublestate-button-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.doublestate-button-element,
.doublestate-button-element-mouse-hover,
.doublestate-button-element-selected,
.doublestate-button-element-selected-mouse-hover {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    background: url('resources/dsb-button.png') no-repeat center;
    cursor: pointer;
}

.doublestate-button-text,
.doublestate-button-image {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    cursor: pointer;
    position: absolute;
}
</css><view>&lt;div class="doublestate-button-wrapper"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="doublestate-button-wrapper"&gt;

&lt;/div&gt;
</preview><presenter>function AddonDouble_State_Button_create(){
    let presenter = function() {};

    let playerController;
    let isWCAGOn = false;
    let eventBus = null;

    presenter.speechTexts = {};
    presenter.setURLIconFromCSS = false;

    const CSS_CLASSES = {
        ELEMENT : "doublestate-button-element",
        MOUSE_HOVER : "doublestate-button-element-mouse-hover",
        SELECTED : "doublestate-button-element-selected",
        SELECTED_MOUSE_HOVER : "doublestate-button-element-selected-mouse-hover",
        IMAGE : "doublestate-button-image",
        TEXT : "doublestate-button-text",
        MOCKED : 'mocked',
    };

    const DEFAULT_TTS_PHRASES = {
        SELECT_BUTTON: "selected",
        DESELECT_BUTTON: "deselected",
        DISABLED_BUTTON: "disabled"
    };

    const ERROR_MESSAGES = {
        INVALID_FILE: 'Invalid SVG file',
    };

    function CSS_CLASSESToString() {
        return CSS_CLASSES.ELEMENT + " " + CSS_CLASSES.MOUSE_HOVER + " "  +
            CSS_CLASSES.SELECTED + " " + CSS_CLASSES.SELECTED_MOUSE_HOVER;
    }

    presenter.DISPLAY_CONTENT_TYPE = {
        NONE: 0,
        TEXT: 1,
        IMAGE: 2,
        BOTH: 3
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeDisable(model);
        upgradedModel = presenter.addImageAlternativeText(upgradedModel);
        upgradedModel = presenter.addLangTag(upgradedModel);
        upgradedModel = presenter.addTTS(upgradedModel);
        upgradedModel = presenter.addRenderSVGAsHTML(upgradedModel);
        upgradedModel = presenter.addOmitTextInTTS(upgradedModel);

        return upgradedModel;
    };

    presenter.upgradeDisable = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["Disable"]) {
            upgradedModel["Disable"] = "False";
        }

        return upgradedModel;
    };

    presenter.addImageAlternativeText = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model["imageAlternativeText"])
            upgradedModel["imageAlternativeText"] = "";

        if (!model["imageSelectedAlternativeText"])
            upgradedModel["imageSelectedAlternativeText"] = "";

        return upgradedModel;
    };

    presenter.addLangTag = function AddonTable_addLangTag(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['langAttribute']) {
            upgradedModel['langAttribute'] =  '';
        }

        return upgradedModel;
    };

    presenter.addTTS = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);


        if (!model["speechTexts"]) {
            upgradedModel["speechTexts"] = {
                selectButton: {selectButton: DEFAULT_TTS_PHRASES.SELECT_BUTTON},
                deselectButton: {deselectButton: DEFAULT_TTS_PHRASES.DESELECT_BUTTON},
                speechTextDisabled: {speechTextDisabled: "False"}
            };
        }

        if (!upgradedModel['speechTexts'].hasOwnProperty('disabledButton')) {
            presenter.useWithoutDisable= true;
            upgradedModel['speechTexts']['disabledButton'] = {disabledButton: DEFAULT_TTS_PHRASES.DISABLED_BUTTON};
        }

        return upgradedModel;
    };

    presenter.addRenderSVGAsHTML = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!model.hasOwnProperty('renderSVGAsHTML')) {
            upgradedModel['renderSVGAsHTML'] = 'False';
        }

        return upgradedModel;
    };

    presenter.addOmitTextInTTS = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!model.hasOwnProperty('omitTextInTTS')) {
            upgradedModel['omitTextInTTS'] = 'False';
        }

        return upgradedModel;
    };


    presenter.executeUserEventCode = function (eventCode) {
        playerController.getCommands().executeEventCode(eventCode);
    };

    presenter.clickHandler = function(event) {
        if (event !== undefined) {
            event.stopPropagation();
        }

        if (presenter.configuration.isErrorMode &amp;&amp; !presenter.configuration.enableCheckMode) {
            return;
        }

        var eventCode;

        if(!presenter.configuration.isDisabled) {
            presenter.configuration.isSelected = !presenter.configuration.isSelected;
            eventCode = presenter.isSelected() ? presenter.configuration.selected.event : presenter.configuration.deselected.event;
            presenter.executeUserEventCode(eventCode);
            presenter.setElementSelection();
            presenter.updateLaTeX();
            presenter.sendEventData();
        }

        handleReplaceSVGIcon();
    };

    function addPointersListeners() {
        const $element = presenter.$view.find('div[class*=doublestate-button-element]:first');

        $element.on('click', (e) =&gt; presenter.clickHandler(e));

        $element.hover(
            function() {
                $(this).removeClass(CSS_CLASSESToString());
                $(this).addClass(presenter.isSelected() ? CSS_CLASSES.SELECTED_MOUSE_HOVER : CSS_CLASSES.MOUSE_HOVER);
            },
            function() {
                $(this).removeClass(CSS_CLASSESToString());
                $(this).addClass(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
            }
        );
    }

    function setElementsDimensions(model, wrapper, element) {
        var viewDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var viewDistances = DOMOperationsUtils.calculateOuterDistances(viewDimensions);
        presenter.$view.css({
            width:(model.Width - viewDistances.horizontal) + 'px',
            height:(model.Height - viewDistances.vertical) + 'px'
        });

        DOMOperationsUtils.setReducedSize(presenter.$view, wrapper);
        DOMOperationsUtils.setReducedSize(wrapper, element);
    }

    presenter.createMockedImages = function () {
        const selectedMockedElement = document.createElement('div');
        const deselectedMockedElement = document.createElement('div');

        $(selectedMockedElement).addClass(CSS_CLASSES.SELECTED);
        $(selectedMockedElement).addClass(CSS_CLASSES.MOCKED);
        $(deselectedMockedElement).addClass(CSS_CLASSES.ELEMENT);
        $(deselectedMockedElement).addClass(CSS_CLASSES.MOCKED);

        createImageElement(selectedMockedElement);
        createImageElement(deselectedMockedElement);

        presenter.$wrapper.append(selectedMockedElement);
        presenter.$wrapper.append(deselectedMockedElement);
    };

    presenter.hideButtonElement = function (element) {
        $(element).attr('style', 'visibility: hidden !important');
    };

    presenter.setIconURLFromMockedImage = function () {
        const $selectedElement = $(presenter.$wrapper.find(`.${CSS_CLASSES.SELECTED}`));
        const $deselectedElement = $(presenter.$wrapper.find(`.${CSS_CLASSES.ELEMENT}`));

        let url = $selectedElement.css('background-image');
        let sanitizeUrl = getURLFromAbsolutePath(url);
        presenter.configuration.selected.image = sanitizeUrl;

        url = $deselectedElement.css('background-image');
        sanitizeUrl = getURLFromAbsolutePath(url);
        presenter.configuration.deselected.image = sanitizeUrl;
        presenter.setURLIconFromCSS = true;
    };

    function createImageElement(element) {
        const imageElement = document.createElement('img');
        $(imageElement).addClass(CSS_CLASSES.IMAGE);
        $(imageElement).attr('src', presenter.isSelected() ? presenter.configuration.selected.image : presenter.configuration.deselected.image);

        $(element).append(imageElement);
    }

    function createSVGElement(element) {
        const url = presenter.isSelected() ? presenter.configuration.selected.image : presenter.configuration.deselected.image;
        const sanitizeUrl = window.xssUtils.sanitize(url);

        $.ajax({
            url: sanitizeUrl,
            success: function (data) {
                onLoadCompleted(data, element);
            },
            error: function () {
                presenter.showErrorMessage();
            },
            dataType: 'xml'
        });
    }

    function createSVGElementFromCSSUrl(element) {
        setTimeout(() =&gt; {
            presenter.setIconURLFromMockedImage(element, true);
            const url = presenter.isSelected() ? presenter.configuration.selected.image : presenter.configuration.deselected.image;

            $.ajax({
                url: url,
                success: function (data) {
                    removeMockedImages();
                    removeBackgroundImage(element);
                    onLoadCompleted(data, element);
                    presenter.addEvents();
                    applySelectionStyle(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
                },
                error: function (err) {
                    removeMockedImages();
                    presenter.showErrorMessage(err);
                }
            });
        }, 200);
    }

    function handleReplaceSVGIcon() {
        if (!presenter.configuration.renderSVGAsHTML) {
            return;
        }

        replaceSVGIcon();
    }

    function replaceSVGIcon() {
        const element = presenter.$wrapper.get(0).children[0];
        const url = getSVGUrl();
        const sanitizeUrl = window.xssUtils.sanitize(url);

        $.ajax({
            url: sanitizeUrl,
            success: function (data) {
                onLoadCompleted(data, element);
            },
            error: function () {
                presenter.showErrorMessage();
            },
            dataType: 'xml'
        });
    }

    function getSVGUrl() {
        return presenter.isSelected() ?
            presenter.configuration.selected.image : presenter.configuration.deselected.image;
    }

    function isAtLeastOneImageAvailable() {
        return presenter.configuration.selected.image || presenter.configuration.deselected.image;
    }

    function getURLFromAbsolutePath(absolutePath) {
        const regExp = new RegExp(".*\\.com(\\/)file");
        const urlMatch = absolutePath.match(regExp);
        const startURLIndex = urlMatch[0].lastIndexOf('/');
        const endURLIndex = absolutePath.lastIndexOf('"');

        return absolutePath.slice(startURLIndex, endURLIndex);
    }

    function removeMockedImages() {
        const images = presenter.$wrapper.find('.mocked');
        $(images[0]).remove();
        $(images[1]).remove();
    }

    function removeBackgroundImage(element) {
        const imageElement = $(element).find('doublestate-button-element');
        const $imageElement = $(imageElement.context);
        $imageElement.attr('style', 'background-image: none !important');
    }

    function hideImageElement(element) {
        const imageElement = $(element).find('doublestate-button-element');
        const $imageElement = $(imageElement.context);
        $imageElement.attr('style', 'visibility: hidden !important');
    }

    function onLoadCompleted(data, element) {
        const svgElement = $(data).find('svg');
        if (svgElement.length === 0) {
            presenter.showErrorMessage();
            return;
        }

        const svgWidth = getElementWidth(svgElement);
        const svgHeight = getElementHeight(svgElement);

        svgElement.attr('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight);
        svgElement.attr('width', '100%');
        svgElement.attr('height', '100%');

        const svgData = data.childNodes;
        $(element).html(svgData);
        createTextElement(element);
    }

    function getElementWidth(HTMLElement) {
        const width = HTMLElement.attr('width');
        const widthFromRectTag = HTMLElement.find('rect').attr('width');

        if (width) {
            return width.replace('px', '');
        } else if (widthFromRectTag) {
            return widthFromRectTag.replace('px', '');
        } else {
            return presenter.configuration.width;
        }
    }

    function getElementHeight(HTMLElement) {
        const height = HTMLElement.attr('width');
        const heightFromRectTag = HTMLElement.find('rect').attr('width');

        if (height) {
            return height.replace('px', '');
        } else if (heightFromRectTag) {
            return heightFromRectTag.replace('px', '');
        } else {
            return presenter.configuration.height;
        }
    }

    presenter.showErrorMessage = function() {
        const errorContainer = '&lt;p&gt;' + ERROR_MESSAGES.INVALID_FILE + '&lt;/p&gt;';

        presenter.$view.html(errorContainer);
    };

    function createTextElement(element) {
        const textElement = document.createElement('span');
        $(textElement).addClass(CSS_CLASSES.TEXT);
        const buttonText = presenter.getSanitizedButtonTextValue();
        $(textElement).html(buttonText);
        $(element).append(textElement);
    }

    function createElements(wrapper) {
        const element = document.createElement('div');
        const atLeastOneImgIsAvailable = isAtLeastOneImageAvailable();
        $(element).addClass(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);

        if (!atLeastOneImgIsAvailable &amp;&amp; presenter.configuration.renderSVGAsHTML) {
            presenter.hideButtonElement(element);
            presenter.createMockedImages();
            createSVGElementFromCSSUrl(element);
        } else if (atLeastOneImgIsAvailable &amp;&amp; presenter.configuration.renderSVGAsHTML) {
            createSVGElement(element);
        } else {
            createImageElement(element);
            createTextElement(element);
        }

        wrapper.append(element);

        return element;
    }

    function presenterLogic(view, model, preview) {
        presenter.$view = $(view);
        presenter.view = view;
        presenter.wrapper = presenter.$view.find('.doublestate-button-wrapper:first')[0];
        presenter.$wrapper = $(presenter.wrapper);


        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        var element = createElements(presenter.$wrapper);

        setElementsDimensions(model, presenter.$wrapper, element);
        presenter.setElementSelection();
        presenter.toggleDisable(presenter.configuration.isDisabled);
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.setTabindex(presenter.$wrapper, presenter.configuration.isTabindexEnabled);

        if (!preview) {
            presenter.addEvents();
            MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }
    }

    presenter.addEvents = function () {
        addPointersListeners();
        presenter.addKeyboardListeners();
    }

    presenter.addKeyboardListeners = function () {
        presenter.wrapper.addEventListener("keydown", presenter.handleKeyboardEvents);
    };

    presenter.handleKeyboardEvents = function AddonDouble_State_Button_handleKeyboardEvents(event) {
        if (window.KeyboardControllerKeys.ENTER === event.keyCode ||
            window.KeyboardControllerKeys.SPACE === event.keyCode
        )   {
            event.preventDefault();
            presenter.clickHandler();
        }
    };

    function applySelectionStyle(className) {
        var element = presenter.$view.find('div[class*=doublestate-button-element]:first');

        $(element).removeClass(CSS_CLASSESToString());
        $(element).addClass(className);
    }

    presenter.setElementSelection = function() {
        const element = presenter.$view.find('div[class*=doublestate-button-element]:first');
        const displayContent = presenter.isSelected() ? presenter.configuration.selected.displayContent : presenter.configuration.deselected.displayContent;

        const textElement = $(element).find('.' + CSS_CLASSES.TEXT);
        const buttonText = presenter.getSanitizedButtonTextValue();
        textElement.html(buttonText);

        const imageElement = $(element).find('.' + CSS_CLASSES.IMAGE);
        imageElement.attr('src', presenter.isSelected() ? presenter.configuration.selected.image : presenter.configuration.deselected.image);

        switch (displayContent) {
            case presenter.DISPLAY_CONTENT_TYPE.NONE:
                $(textElement).hide();
                $(imageElement).hide();
                break;
            case presenter.DISPLAY_CONTENT_TYPE.TEXT:
                $(textElement).show();
                $(imageElement).hide();
                break;
            case presenter.DISPLAY_CONTENT_TYPE.IMAGE:
                $(imageElement).show();
                $(textElement).hide();
                break;
            case presenter.DISPLAY_CONTENT_TYPE.BOTH:
                $(imageElement).show();
                $(textElement).show();
                break;
        }

        applySelectionStyle(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
    };

    presenter.getSanitizedButtonTextValue = function () {
        const text = presenter.isSelected() ? presenter.configuration.selected.text : presenter.configuration.deselected.text
        return window.xssUtils.sanitize(text);
    }

    presenter.updateLaTeX = function () {
        const textElement = presenter.$view.find('.' + CSS_CLASSES.TEXT)[0];
        presenter.mathJaxProcessEnded.then(function () {
                const args = [];
                args.push("Typeset", MathJax.Hub, textElement);
                MathJax.Hub.Queue(args);
        });
    };

    presenter.select = function () {
        if(!presenter.configuration.isSelected){
            presenter.configuration.isSelected = true;
            presenter.setElementSelection();
            presenter.updateLaTeX();
            handleReplaceSVGIcon();
        }
    };

    presenter.deselect = function () {
        if(presenter.configuration.isSelected){
            presenter.configuration.isSelected = false;
            presenter.setElementSelection();
            presenter.updateLaTeX();
            handleReplaceSVGIcon();
        }
    };

    presenter.isSelected = function () {
        return presenter.configuration.isSelected;
    };

    presenter.executeCommand = function(name) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'select': presenter.select,
            'deselect': presenter.deselect,
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enable,
            'disable': presenter.disable,
            'isSelected': presenter.isSelected
        };

        Commands.dispatch(commands, name, [], presenter);
    };

    presenter.isIE9 = function (userAgent) {
        userAgent = userAgent.toLowerCase();

        if (userAgent.indexOf('msie') != -1) {
            return parseInt(userAgent.split('msie')[1], 10) == 9;
        }

        return false;
    };

    presenter.setVisibility = function(isVisible) {
        if(presenter.isIE9(navigator.userAgent)) {
            presenter.$view.css('display', isVisible ? 'block' : "none");
        }

        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;

        var mathJaxDeferred = new jQuery.Deferred();
        presenter.mathJaxProcessEndedDeferred = mathJaxDeferred;
        presenter.mathJaxProcessEnded = mathJaxDeferred.promise();

        MathJax.Hub.Register.MessageHook("End Process", function (message) {
            if ($(message[1]).hasClass('ic_page')) {
                if(presenter.mathJaxProcessEndedDeferred.state() != 'resolved'){
                    presenter.mathJaxProcessEndedDeferred.resolve();
                }
            }
        });
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    }

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.show = function() {
        if (!presenter.configuration.isVisible) {
            presenter.setVisibility(true);
            presenter.configuration.isVisible = true;
            presenter.setElementSelection();
            presenter.updateLaTeX();
        }
    };

    presenter.hide = function() {
        if (presenter.configuration.isVisible) {
            presenter.setVisibility(false);
            presenter.configuration.isVisible = false;
            presenter.setElementSelection();
            presenter.updateLaTeX();
        }
    };

    presenter.isEnabledInGSAMode = function () {
        return presenter.configuration.enableCheckMode;
    }

    presenter.enable = function() {
        if (presenter.configuration.isDisabled) {
            presenter.toggleDisable(false);
            presenter.setElementSelection();
            presenter.updateLaTeX();
        }
    };

    presenter.disable = function() {
        if (!presenter.configuration.isDisabled) {
            presenter.toggleDisable(true);
            presenter.setElementSelection();
            presenter.updateLaTeX();
        }
    };

    presenter.toggleDisable = function(disable) {
        var element = presenter.$view.find('div[class*=doublestate-button-element]:first');

        if(disable) {
            element.addClass("disable");
        } else {
            element.removeClass("disable");
        }

        presenter.configuration.isDisabled = disable;
    };

    presenter.reset = function() {
        presenter.configuration.isSelected = presenter.configuration.isSelectedByDefault;
        presenter.configuration.isErrorMode = false;

        presenter.setElementSelection();

        if (presenter.configuration.isVisibleByDefault) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.toggleDisable(presenter.configuration.isDisabledByDefault);
        presenter.updateLaTeX();
        handleReplaceSVGIcon();
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            isSelected: presenter.configuration.isSelected,
            isDisabled: presenter.configuration.isDisabled,
            selectedImage: presenter.configuration.selected.image,
            deselectedImage: presenter.configuration.deselected.image,
            setURLIconFromCSS: presenter.setURLIconFromCSS
        });
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsedState = JSON.parse(state);

        if (parsedState.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.toggleDisable(parsedState.isDisabled);

        presenter.configuration.isSelected = parsedState.isSelected;

        if (parsedState.selectedImage || parsedState.deselectedImage) {
            presenter.configuration.selected.image = parsedState.selectedImage;
            presenter.configuration.deselected.image = parsedState.deselectedImage;
        }

        presenter.setElementSelection();

        if (!parsedState.setURLIconFromCSS) {
            handleReplaceSVGIcon();
        }
    };

    presenter.validateString = function (imageSrc) {
        var isEmpty = ModelValidationUtils.isStringEmpty(imageSrc);

        return {
            isEmpty: isEmpty,
            value: isEmpty ? "" : imageSrc
        };
    };

    presenter.determineDisplayContent = function(text, image) {
        var displayContent = presenter.DISPLAY_CONTENT_TYPE.NONE;
        if (!text.isEmpty &amp;&amp; image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.TEXT;
        } else if (text.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.IMAGE;
        } else if (!text.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.BOTH;
        }

        return displayContent;
    };

    presenter.validateModel = function (model) {
        var text = presenter.validateString(model.Text);
        var image = presenter.validateString(model.Image);
        var selectedText = presenter.validateString(model["Text selected"]);
        var selectedImage = presenter.validateString(model["Image selected"]);
        var imageAlternativeText = presenter.validateString(model["imageAlternativeText"]);
        var imageSelectedAlternativeText = presenter.validateString(model["imageSelectedAlternativeText"]);
        var langTag = presenter.validateString(model["langAttribute"]);

        var isDisabled = ModelValidationUtils.validateBoolean(model.Disable);
        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        var isSelected = ModelValidationUtils.validateBoolean(model.isSelected);
        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]);
        var enableCheckMode = ModelValidationUtils.validateBoolean(model["Do not block in check mode"]);
        var renderSVGAsHTML = ModelValidationUtils.validateBoolean(model.renderSVGAsHTML);
        var omitTextInTTS = ModelValidationUtils.validateBoolean(model.omitTextInTTS);
        var width = +model.Width;
        var height = +model.Height;

        presenter.setSpeechTexts(model['speechTexts']);

        return {
            addonID: model.ID,
            selected: {
                text: selectedText.value,
                image: selectedImage.value,
                imageAlternativeText: imageSelectedAlternativeText.value,
                event: model.onSelected,
                displayContent: presenter.determineDisplayContent(selectedText, selectedImage)
            },
            deselected: {
                text: text.value,
                image: image.value,
                imageAlternativeText: imageAlternativeText.value,
                event: model.onDeselected,
                displayContent: presenter.determineDisplayContent(text, image)
            },
            isSelected: isSelected,
            isSelectedByDefault: isSelected,
            isDisabled: isDisabled,
            isDisabledByDefault: isDisabled,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isErrorMode: false,
            isTabindexEnabled: isTabindexEnabled,
            enableCheckMode: enableCheckMode,
            langTag: langTag,
            renderSVGAsHTML: renderSVGAsHTML,
            width: width,
            height: height,
            omitTextInTTS: omitTextInTTS
        };
    };

    presenter.setSpeechTexts = function (speechTexts) {
        speechTexts = getSpeechTexts(speechTexts);

        presenter.speechTexts = {
            selectButton: getSpeechTextProperty(speechTexts.selectButton.selectButton, DEFAULT_TTS_PHRASES.SELECT_BUTTON),
            deselectButton: getSpeechTextProperty(speechTexts.deselectButton.deselectButton, DEFAULT_TTS_PHRASES.DESELECT_BUTTON),
            speechTextDisabled: ModelValidationUtils.validateBoolean(speechTexts.speechTextDisabled.speechTextDisabled),
            disabledButton: getSpeechTextProperty(speechTexts.disabledButton.disabledButton, DEFAULT_TTS_PHRASES.DISABLED_BUTTON)
        };
    };

    function getSpeechTexts (speechTexts) {
        if (!speechTexts) {
            speechTexts = {
                selectButton: {selectButton: DEFAULT_TTS_PHRASES.SELECT_BUTTON},
                deselectButton: {deselectButton: DEFAULT_TTS_PHRASES.DESELECT_BUTTON},
                speechTextDisabled: {speechTextDisabled: "False"}
            };
        }

        if (!speechTexts.hasOwnProperty('disabledButton')) {
            speechTexts['disabledButton'] = {disabledButton: DEFAULT_TTS_PHRASES.DISABLED_BUTTON}
        }

        return speechTexts;
    }

    function getSpeechTextProperty(value, defaultValue) {
        if (value === undefined || value === null || value.trim() === '')
            return defaultValue;
        return value.trim();
    }

    presenter.createEventData = function() {
        return {
            source : presenter.configuration.addonID,
            item : '',
            value : presenter.isSelected() ? '1' : '0',
            score : ''
        };
    };

    presenter.sendEventData = function () {
        const eventData = presenter.createEventData();
        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.setShowErrorsMode = function () {
        presenter.configuration.isErrorMode = true;
    };

    presenter.setWorkMode = function () {
        presenter.configuration.isErrorMode = false;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.configuration.isErrorMode = true;
        }

        if (eventName == "HideAnswers") {
            presenter.configuration.isErrorMode = false;
        }
    };

    presenter.keyboardController = function (keyCode, isShift, event) {
        if (event) {
            event.stopPropagation();

            if (keyCode === window.KeyboardControllerKeys.SPACE ||
                keyCode === window.KeyboardControllerKeys.ARROW_UP ||
                keyCode === window.KeyboardControllerKeys.ARROW_DOWN ||
                keyCode === window.KeyboardControllerKeys.ESCAPE
            ) {
                event.preventDefault();
            }
        }

        if (keyCode === window.KeyboardControllerKeys.ENTER) {
            if (isWCAGOn)
                presenter.speakEnterAction();
        }

        if (keyCode == window.KeyboardControllerKeys.SPACE) {
            presenter.clickHandler();
            if(presenter.canSpeakSpaceAction())
                presenter.speakSpaceAction();
        }
    };

    presenter.canSpeakSpaceAction = function() {
        return isWCAGOn &amp;&amp; !presenter.configuration.isErrorMode &amp;&amp; !presenter.configuration.enableCheckMode &amp;&amp; !presenter.speechTexts.speechTextDisabled;
    };

    presenter.speakEnterAction = function() {
        var textVoices = [];
        var lang = presenter.configuration.langTag.value;

        if (presenter.configuration.isSelected) {
            if (!presenter.configuration.omitTextInTTS) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.selected.text, lang));
            }

            var imageAlternativeText = presenter.configuration.selected.imageAlternativeText;
            if (imageAlternativeText)
                textVoices.push(window.TTSUtils.getTextVoiceObject(imageAlternativeText));

            textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selectButton));
        } else {
            if (!presenter.configuration.omitTextInTTS) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.deselected.text, lang));
            }
            var imageAlternativeText = presenter.configuration.deselected.imageAlternativeText;
            if (imageAlternativeText)
                textVoices.push(window.TTSUtils.getTextVoiceObject(imageAlternativeText))
        }

        if (presenter.configuration.isDisabled &amp;&amp; !presenter.useWithoutDisable) {
            textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.disabledButton))
        }

        speak(textVoices);
    };

    presenter.speakSpaceAction = function() {
        var textVoices = [];
        if (presenter.configuration.isDisabled &amp;&amp; !presenter.useWithoutDisable) {
            textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.disabledButton))
        } else {
            if (presenter.configuration.isSelected) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selectButton));
            } else {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.deselectButton));
            }
        }

        speak(textVoices);
    };

    presenter.setTabindex = function (element, isTabindexEnabled) {
        var tabindexValue = isTabindexEnabled ? "0" : "-1";
        element.attr("tabindex", tabindexValue);
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts) {
            tts.speak(data);
        }
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.getTitlePostfix = function () {
        if(presenter.configuration.isSelected) {
            return presenter.speechTexts.selected;
        } else {
            return ''
        }
    };

    presenter.destroy = function(event) {
        if (event.target === presenter.view) {
            presenter.wrapper.removeEventListener("keydown", presenter.handleKeyboardEvents);
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Drawing" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Color" nameLabel="Drawing_property_color" type="string"/>
		<property name="Thickness" nameLabel="Drawing_property_thickness" type="string"/>
		<property name="Border" nameLabel="Drawing_property_border" type="string"/>
		<property name="Opacity" nameLabel="Drawing_property_opacity" type="string"/>
		<property name="Font" nameLabel="Drawing_property_font" type="string"/>
		<!-- <property name='Whiteboard mode' nameLabel="Drawing_property_whiteboard_mode" type='boolean'/> -->
	</model>
<css>.drawing .tmp_canvas {
    position: absolute;
    left: 0; right: 0;
    bottom: 0; top: 0;

    cursor: crosshair;

    -ms-touch-action: none;
    touch-action: none;
}

.drawing .tmp_canvas:active {
    cursor: crosshair;
}
</css><view>&lt;div class="drawing"&gt;&lt;/div&gt;
</view><preview>&lt;div class="drawing"&gt;&lt;/div&gt;
</preview><presenter>function AddonDrawing_create() {

    var presenter = function() {};
    var eventBus = null;

    const ModeEnum = {
        pencil: "pencil",
        eraser: "eraser",
        imageEdition: "imageEdition",
        textEdition: "textEdition",
    };

    const AnchorEnum = {
        topLeft: 0,
        topRight: 1,
        bottomLeft: 2,
        bottomRight: 3,
    };

    presenter.playerController = null;
    presenter.imageInputElement = null;

    presenter.$textWrapper = null;

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;
    };

    function setDefaultAddonMode() {
        setAddonMode(ModeEnum.pencil);
    }

    function setAddonMode(addonMode) {
        if (isOnTextEditionMode()) {
            presenter.closeTextFieldPopup();
        }
        presenter.configuration.addonMode = addonMode;
    }

    function isOnPencilMode() {
        return presenter.configuration.addonMode === ModeEnum.pencil;
    }

    function isOnEraserMode() {
        return presenter.configuration.addonMode === ModeEnum.eraser;
    }

    function isOnImageEditionMode() {
        return presenter.configuration.addonMode === ModeEnum.imageEdition;
    }

    function isOnTextEditionMode() {
        return presenter.configuration.addonMode === ModeEnum.textEdition;
    }

    // work-around for double line in android browser
    function setOverflowWorkAround(turnOn) {

        if (!MobileUtils.isAndroidWebBrowser(window.navigator.userAgent)) { return false; }

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {

            presenter.$view.parents("*").each(function() {
                var overflow = null;
                if (turnOn) {
                    $(this).attr("data-overflow", $(this).css("overflow"));
                    $(this).css("overflow", "visible");
                } else {
                    overflow = $(this).attr("data-overflow");
                    if (overflow !== "") {
                        $(this).css("overflow", overflow);
                    }
                    $(this).removeAttr("data-overflow");
                }
            });

        }

        return true;
    }

    presenter.points = [];
    presenter.pointerPosition = {x: 0, y: 0};
    presenter.shouldUpdateState = false;
    presenter.isModified = false;
    let shouldSendModifiedEventOnLeave = false;
    let shouldSkipNextModifiedEvent = false;

    presenter.addedImage = {};
    presenter.draggingAnchor = {
        x: 0,
        y: 0
    };
    presenter.draggingImage = false;
    var pi2 = Math.PI * 2;
    var resizerRadius = 8;
    var rr = resizerRadius * resizerRadius;

    function getZoom() {
        var val = $('#_icplayer').css('zoom');
        if (val == "normal" || val == "") { // IE 11
            val = 1;
        }

        val = parseInt(val, 10);
        if (val == NaN || val == undefined) {
            val = 1;
        }
        return val;
    }

    function getScale() {
        if (presenter.playerController) {
            const scaleInformation = presenter.playerController.getScaleInformation();
            if (scaleInformation.baseScaleX !== 1.0 ||
                scaleInformation.baseScaleY !== 1.0 ||
                scaleInformation.scaleX !== 1.0 ||
                scaleInformation.scaleY !== 1.0
            ) {
                return {X: scaleInformation.scaleX, Y: scaleInformation.scaleY};
            }
        }

        const $content = $("#content");
        if ($content.size() &gt; 0) {
            const contentElem = $content[0];
            const scaleX = contentElem.getBoundingClientRect().width / contentElem.offsetWidth;
            const scaleY = contentElem.getBoundingClientRect().height / contentElem.offsetHeight;
            return {X: scaleX, Y: scaleY};
        } else {
            return {X: 1.0, Y: 1.0};
        }
    }

    presenter.hexToRGBA = function(hex, opacity) {
        hex = hex.replace('#', '');
        var r = parseInt(hex.substring(0,2), 16);
        var g = parseInt(hex.substring(2,4), 16);
        var b = parseInt(hex.substring(4,6), 16);

        return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
    };

    presenter.colourNameToHex = function(color) {

        var colors = {
            "aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff","beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887","cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff","darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f","darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1","darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff","firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff","gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f","honeydew":"#f0fff0","hotpink":"#ff69b4","indianred ":"#cd5c5c","indigo ":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c","lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2","lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de","lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6","magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee","mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5","navajowhite":"#ffdead","navy":"#000080","oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6","palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080","red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1","saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4","tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0","violet":"#ee82ee","wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5","yellow":"#ffff00","yellowgreen":"#9acd32"
        };

        if (typeof colors[color.toLowerCase()] !== 'undefined') {
            return colors[color.toLowerCase()];
        }

        return false;
    };

    presenter.onMobilePaint = function(e) {
        e.stopPropagation();
        e.preventDefault();
        var tmp_canvas;
        if (isOnPencilMode()) {
            tmp_canvas = presenter.configuration.tmp_canvas;
        } else {
            tmp_canvas = presenter.configuration.canvas;
        }

        var x = e.targetTouches[0].pageX - $(tmp_canvas).offset().left;
        var y = e.targetTouches[0].pageY - $(tmp_canvas).offset().top;

        if (presenter.zoom !== 1) {
            x = x * (1 / presenter.zoom);
            y = y * (1 / presenter.zoom);
        }

        var scale = getScale();
        if(scale.X!==1.0 || scale.Y!==1.0){
            x = x/scale.X;
            y = y/scale.Y;
        }

        presenter.pointerPosition.x = x;
        presenter.pointerPosition.y = y;
        presenter.onPaint(e);
    };

    presenter.onMobileImageEdition = function(e) {
        e.stopPropagation();
        e.preventDefault();
        var tmp_canvas = presenter.configuration.tmp_canvas;

        var x = e.targetTouches[0].pageX - $(tmp_canvas).offset().left;
        var y = e.targetTouches[0].pageY - $(tmp_canvas).offset().top;

        if (presenter.zoom !== 1) {
            x = x * (1 / presenter.zoom);
            y = y * (1 / presenter.zoom);
        }

        var scale = getScale();
        if(scale.X!==1.0 || scale.Y!==1.0){
            x = x/scale.X;
            y = y/scale.Y;
        }

        presenter.pointerPosition.x = x;
        presenter.pointerPosition.y = y;
        presenter.onImageEdition(e);
    };

    function anchorHitTest(x, y, image) {

        var dx, dy;
    
        // top-left
        dx = x - image.left;
        dy = y - image.top;
        if (dx * dx + dy * dy &lt;= rr) {
            return AnchorEnum.topLeft;
        }
        // top-right
        dx = x - (image.left + image.width);
        dy = y - image.top;
        if (dx * dx + dy * dy &lt;= rr) {
            return AnchorEnum.topRight;
        }
        // bottom-right
        dx = x - (image.left + image.width);
        dy = y - (image.top + image.height);
        if (dx * dx + dy * dy &lt;= rr) {
            return AnchorEnum.bottomRight;
        }
        // bottom-left
        dx = x - image.left;
        dy = y - (image.top + image.height);
        if (dx * dx + dy * dy &lt;= rr) {
            return AnchorEnum.bottomLeft;
        }
        return (-1);
    }

    function hitImage(x, y, image) {
        return (x &gt; image.left &amp;&amp; x &lt; image.left + image.width &amp;&amp; y &gt; image.top &amp;&amp; y &lt; image.top + image.height);
    }

    function drawDragAnchor(x, y) {

        presenter.configuration.tmp_ctx.beginPath();
        presenter.configuration.tmp_ctx.arc(x, y, resizerRadius, 0, pi2, false);
        presenter.configuration.tmp_ctx.closePath();
        presenter.configuration.tmp_ctx.fill();
    }

    presenter.drawImage = function (tmp_ctx, tmp_canvas, withAnchors, withBorders, image) {
        // clear the canvas
        tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        // draw image
        tmp_ctx.drawImage(image.image, 0, 0, image.originalWidth, image.originalHeight, image.left, image.top, image.width, image.height);
        
        // optionally draw the draggable anchors
        if (withAnchors) {
            drawDragAnchor(image.left, image.top);
            drawDragAnchor(image.left + image.width, image.top);
            drawDragAnchor(image.left + image.width, image.top + image.height);
            drawDragAnchor(image.left, image.top + image.height);
        }

        // optionally draw the connecting anchor lines
        if (withBorders) {
            tmp_ctx.beginPath();
            tmp_ctx.moveTo(image.left, image.top);
            tmp_ctx.lineTo(image.left + image.width, image.top);
            tmp_ctx.lineTo(image.left + image.width, image.top + image.height);
            tmp_ctx.lineTo(image.left, image.top + image.height);
            tmp_ctx.closePath();
            tmp_ctx.stroke();
        }
    };

    presenter.addImage = function (img) {
        presenter.setEraserOff();
        setAddonMode(ModeEnum.imageEdition);
        var tmp_canvas = presenter.configuration.tmp_canvas;
        var tmp_ctx = presenter.configuration.tmp_ctx;
        var image = {};
        var widthRatio = 1;
        var heightRatio = 1;
        if (img.width &gt; tmp_canvas.width) {
            widthRatio = tmp_canvas.width / img.width;
            heightRatio = widthRatio;
        }
        if (img.height &gt; tmp_canvas.height){
            var tempHeightRatio = tmp_canvas.height / img.height;
            if (Math.fround(tempHeightRatio) &lt; Math.fround(heightRatio))
            {
                widthRatio = tempHeightRatio;
                heightRatio = tempHeightRatio;
            }
        }

        image.width = img.width * widthRatio;
        image.originalWidth = img.width;
        image.height = img.height * heightRatio;
        image.originalHeight = img.height;
        image.left = tmp_canvas.width / 2 - image.width / 2;
        image.top = tmp_canvas.height / 2 - image.height / 2;
        image.image = img;
        image.showUpMoment = presenter.points.length;
        presenter.addedImage = image;
        
        // draw for first time
        presenter.shouldUpdateState = true;
        presenter.isModified = true;
        tmp_ctx.drawImage(image.image, 0, 0, image.originalWidth, image.originalHeight, image.left, image.top, image.width, image.height);

        drawDragAnchor(image.left, image.top);
        drawDragAnchor(image.left + image.width, image.top);
        drawDragAnchor(image.left + image.width, image.top + image.height);
        drawDragAnchor(image.left, image.top + image.height);
    };

    presenter.embedText = function(textArray, x, y, lineHeight) {
        if (presenter.configuration.font) presenter.configuration.tmp_ctx.font = presenter.configuration.font;
        presenter.configuration.tmp_ctx.clearRect(0, 0, presenter.configuration.tmp_canvas.width, presenter.configuration.tmp_canvas.height);
        presenter.configuration.tmp_ctx.fillStyle = presenter.configuration.color;
        for(var i = 0; i &lt; textArray.length; i++) {
            presenter.configuration.tmp_ctx.fillText(textArray[i], x, y + (i+1) * lineHeight);
        }
        presenter.configuration.context.drawImage(presenter.configuration.tmp_canvas, 0, 0);
        presenter.configuration.tmp_ctx.clearRect(0, 0, presenter.configuration.tmp_canvas.width, presenter.configuration.tmp_canvas.height);
    };

    presenter.onPaint = function(e) {
        e.stopPropagation();
        e.preventDefault();
        var tmp_canvas, tmp_ctx;

        if (isOnPencilMode()) {
            tmp_canvas = presenter.configuration.tmp_canvas;
            tmp_ctx = presenter.configuration.tmp_ctx;
            tmp_ctx.globalAlpha = presenter.configuration.opacity;
        } else if (isOnEraserMode()) {
            tmp_canvas = presenter.configuration.canvas;
            tmp_ctx = presenter.configuration.context;
        }

        tmp_ctx.lineWidth = presenter.configuration.thickness;
        tmp_ctx.lineJoin = 'round';
        tmp_ctx.lineCap = 'round';
        tmp_ctx.strokeStyle = presenter.configuration.color;
        tmp_ctx.fillStyle = presenter.configuration.color;

        presenter.points.push({x: presenter.pointerPosition.x, y: presenter.pointerPosition.y});
        if (presenter.points.length &lt; 3) {
            var b = presenter.points[0];
            tmp_ctx.beginPath();
            tmp_ctx.arc(b.x, b.y, tmp_ctx.lineWidth / 2, 0, Math.PI * 2, !0);
            tmp_ctx.fill();
            tmp_ctx.closePath();
        } else {
            tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);

            tmp_ctx.beginPath();
            tmp_ctx.moveTo(presenter.points[0].x, presenter.points[0].y);

            for (var i = 1; i &lt; presenter.points.length - 2; i++) {
                var c = (presenter.points[i].x + presenter.points[i + 1].x) / 2;
                var d = (presenter.points[i].y + presenter.points[i + 1].y) / 2;

                tmp_ctx.quadraticCurveTo(presenter.points[i].x, presenter.points[i].y, c, d);
            }

            tmp_ctx.quadraticCurveTo(
                presenter.points[i].x,
                presenter.points[i].y,
                presenter.points[i + 1].x,
                presenter.points[i + 1].y
            );
            tmp_ctx.stroke();
        }
    };

    presenter.onImageEdition = function(e) {
        e.stopPropagation();
        e.preventDefault();
        var tmp_canvas, tmp_ctx;

        tmp_canvas = presenter.configuration.tmp_canvas;
        tmp_ctx = presenter.configuration.tmp_ctx;
        tmp_ctx.globalAlpha = presenter.configuration.opacity;
        presenter.points.push({x: presenter.pointerPosition.x, y: presenter.pointerPosition.y});
        if (presenter.points.length &lt; 4) {
            presenter.draggingAnchor = anchorHitTest(presenter.points[0].x, presenter.points[0].y, presenter.addedImage);
            presenter.draggingImage = presenter.draggingAnchor &lt; 0 &amp;&amp; hitImage(presenter.points[0].x, presenter.points[0].y, presenter.addedImage);
            if( presenter.draggingAnchor == -1 &amp;&amp; !presenter.draggingImage){
                presenter.finishEditImageMode(tmp_ctx, tmp_canvas, false);
            }
        } else {
            if (presenter.draggingAnchor &gt; -1) {
                let positionX = presenter.points[presenter.points.length - 1].x;
                let positionY = presenter.points[presenter.points.length - 1].y;
                switch (presenter.draggingAnchor) {                    
                    case AnchorEnum.topLeft:
                        //top-left
                        presenter.addedImage.width = (presenter.addedImage.left + presenter.addedImage.width) - positionX;
                        presenter.addedImage.height = (presenter.addedImage.top + presenter.addedImage.height) - positionY;
                        presenter.addedImage.left = positionX;
                        presenter.addedImage.top = positionY;
                        break;
                    case AnchorEnum.topRight:
                        //top-right
                        presenter.addedImage.width = positionX - presenter.addedImage.left;
                        presenter.addedImage.height = (presenter.addedImage.top + presenter.addedImage.height) - positionY;
                        presenter.addedImage.top = positionY;
                        break;
                    case AnchorEnum.bottomRight:
                        //bottom-right
                        presenter.addedImage.width = positionX - presenter.addedImage.left;
                        presenter.addedImage.height = positionY - presenter.addedImage.top;
                        break;
                    case AnchorEnum.bottomLeft:
                        //bottom-left
                        presenter.addedImage.width = (presenter.addedImage.left + presenter.addedImage.width) - positionX;
                        presenter.addedImage.height = positionY - presenter.addedImage.top;
                        presenter.addedImage.left = positionX;
                        break;
                }

                if(presenter.addedImage.width&lt;25){presenter.addedImage.width=25;}
                if(presenter.addedImage.height&lt;25){presenter.addedImage.height=25;}
                
                presenter.drawImage(tmp_ctx, tmp_canvas, true, true, presenter.addedImage);

            } else if (presenter.draggingImage) {
                var dx = presenter.points[presenter.points.length - 1].x - presenter.points[presenter.points.length - 2].x;
                var dy = presenter.points[presenter.points.length - 1].y - presenter.points[presenter.points.length - 2].y;
                presenter.addedImage.left += dx;
                presenter.addedImage.top += dy;
                // draw the image
                presenter.drawImage(tmp_ctx, tmp_canvas, true, true, presenter.addedImage);
            }
        }
    };

    presenter.removeImage = function (event) {
        if (event.key === "Delete" &amp;&amp; presenter.configuration.addonMode === ModeEnum.imageEdition) {
            presenter.finishEditImageMode(
                presenter.configuration.tmp_ctx,
                presenter.configuration.tmp_canvas,
                true
            );
        }
        event.stopPropagation();
    };

    presenter._turnOnEventListeners = function() {
        const tmp_canvas = presenter.configuration.tmp_canvas,
            tmp_ctx = presenter.configuration.tmp_ctx,
            ctx = presenter.configuration.context;


        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported('touchstart')) {
            presenter._connectPointerEvents(tmp_canvas, tmp_ctx, ctx);
        } else {
            presenter._connectTouchEvents(tmp_canvas, tmp_ctx, ctx);
        }

        tmp_canvas.addEventListener("click", presenter.onTmpCanvasClick, false);

        // KEYBOARD DELETE EDIT IMAGE 
        document.addEventListener("keydown", presenter.removeImage, false);
    };

    presenter.onTmpCanvasClick = function (event) {
        event.stopPropagation();
    };

    presenter.onMobilePaintWithoutPropagation = function (e) {
        e.stopPropagation();
        e.preventDefault();
        presenter.onMobilePaint(e);
    };

    function addImageToCanvasIfOnImageEditionMode(){
        if (isOnImageEditionMode()){
            presenter.finishEditImageMode(presenter.configuration.tmp_ctx, presenter.configuration.tmp_canvas, false);
        }
    }

    presenter.finishEditImageMode = function (tmp_ctx, tmp_canvas, rejectAdding) {
        setDefaultAddonMode();
        setOverflowWorkAround(false);
        tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onImageEdition, false);
        tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        if(!rejectAdding){
            presenter.drawImage(tmp_ctx, tmp_canvas, false, false, presenter.addedImage);
            presenter.configuration.context.drawImage(tmp_canvas, 0, 0);
            presenter.isModified = true;
            sendModifiedEvent();
            shouldSkipNextModifiedEvent = true;
        }
        tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        presenter.points = [];
    };

    presenter._connectTouchEvents = function (tmp_canvas, tmp_ctx, ctx) {
        tmp_canvas.addEventListener('touchstart', function (event) {
            setOverflowWorkAround(true);

            if (isOnEraserMode()) {
                presenter.configuration.context.globalCompositeOperation = "destination-out";
            }

            presenter.zoom = getZoom();
            presenter.shouldUpdateState = true;
            if (isOnPencilMode() || isOnEraserMode()) {
                presenter.isModified = true;
                shouldSendModifiedEventOnLeave = true;
                presenter.onMobilePaint(event);
                tmp_canvas.addEventListener('touchmove', presenter.onMobilePaintWithoutPropagation);
            } else if (isOnImageEditionMode()) {
                presenter.isModified = true;
                presenter.onMobileImageEdition(event);
                tmp_canvas.addEventListener('touchmove', presenter.onMobileImageEdition);
            } else if (isOnTextEditionMode()) {
                presenter.finishEditTextMode();
            }
        }, false);

        tmp_canvas.addEventListener('touchend', function (event) {
            setOverflowWorkAround(false);

            tmp_canvas.removeEventListener('touchmove', presenter.onMobilePaintWithoutPropagation, false);
            tmp_canvas.removeEventListener('touchmove', presenter.onMobileImageEdition, false);
            
            if (isOnPencilMode() || isOnEraserMode()) {
                ctx.drawImage(tmp_canvas, 0, 0);
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                shouldSendModifiedEventOnLeave = false;
                sendModifiedEvent();
            } else if (isOnImageEditionMode()) {
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                presenter.drawImage(tmp_ctx, tmp_canvas, true, false, presenter.addedImage);
                sendModifiedEvent();
            }
            presenter.points = [];
        }, false);
    };

    presenter._connectPointerEvents = function (tmp_canvas, tmp_ctx, ctx) {
        tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event) || isOnTextEditionMode()) {
                return;
            }

            event.stopPropagation();
            const position = getPosition(event);
            presenter.pointerPosition.x = position.x;
            presenter.pointerPosition.y = position.y;

            presenter.configuration.tmp_canvas.style.cursor = 'crosshair';
            if (isOnImageEditionMode() &amp;&amp; presenter.addedImage.image !== undefined) {
                if (hitImage(position.x, position.y, presenter.addedImage)) {
                    presenter.configuration.tmp_canvas.style.cursor = 'pointer';
                } else {
                    const anchorIndex = anchorHitTest(position.x, position.y, presenter.addedImage);
                    if (anchorIndex == 0 || anchorIndex == 2) {
                        presenter.configuration.tmp_canvas.style.cursor = 'nwse-resize';
                    } else if (anchorIndex == 1 || anchorIndex == 3) {
                        presenter.configuration.tmp_canvas.style.cursor = 'nesw-resize';
                    }

                }
            }
        }, false);

        tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.LEAVE, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            setOverflowWorkAround(false);
            if (isOnPencilMode() || isOnEraserMode()) {
                tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onPaint, false);
                ctx.drawImage(tmp_canvas, 0, 0);
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                if(shouldSendModifiedEventOnLeave) {
                    sendModifiedEvent();
                    shouldSendModifiedEventOnLeave = false;
                }
            } else if (isOnImageEditionMode()) {
                tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onImageEdition, false);
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                presenter.drawImage(tmp_ctx, tmp_canvas, true, false, presenter.addedImage);
            }
            presenter.points = [];
        });

        tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.DOWN, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            setOverflowWorkAround(true);
            if (isOnEraserMode()) {
                presenter.configuration.context.globalCompositeOperation = "destination-out";
            }

            presenter.zoom = getZoom();
            if (presenter.zoom == "" || presenter.zoom == undefined) {
                presenter.zoom = 1;
            }

            if (isOnPencilMode() || isOnEraserMode()) {
                shouldSendModifiedEventOnLeave = true;
                tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onPaint, false);
            } else if (isOnImageEditionMode()) {
                tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onImageEdition, false);
            }
            presenter.shouldUpdateState = true;

            const position = getPosition(event);
            presenter.pointerPosition.x = position.x;
            presenter.pointerPosition.y = position.y;
            presenter.points.push(position);

            if (isOnPencilMode() || isOnEraserMode()) {
                presenter.isModified = true;
                presenter.onPaint(event);
            } else if (isOnImageEditionMode()) {
                presenter.isModified = true;
                presenter.onImageEdition(event);
            } else if (isOnTextEditionMode()) {
                presenter.finishEditTextMode();
            }
        }, false);

        tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.UP, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            setOverflowWorkAround(false);
            if (isOnPencilMode() || isOnEraserMode()) {
                tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onPaint, false);
                ctx.drawImage(tmp_canvas, 0, 0);
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                shouldSendModifiedEventOnLeave = false;
                sendModifiedEvent();
            } else if (isOnImageEditionMode()) {
                tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onImageEdition, false);
                tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
                presenter.drawImage(tmp_ctx, tmp_canvas, true, false, presenter.addedImage);
            }
            presenter.points = [];
        }, false);
    };

    function getPosition(event) {
        let x = typeof event.offsetX !== 'undefined' ? event.offsetX : event.layerX;
        let y = typeof event.offsetY !== 'undefined' ? event.offsetY : event.layerY;

        if (presenter.zoom !== 1) {
            x = x * (1 / parseInt(presenter.zoom, 10));
            y = y * (1 / parseInt(presenter.zoom, 10));
        }

        return {x: x, y: y};
    }

    function getErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    presenter.ERROR_CODES = {
        C01: 'Property color cannot be empty',
        C02: 'Property color has wrong length in hex format, should be # and 6 digits [0 - F]',
        C03: 'Property color has wrong color name',

        T01: 'Property thickness cannot be empty',
        T02: 'Property thickness cannot be smaller than 1',
        T03: 'Property thickness cannot be bigger than 40',

        B01: 'Property border cannot be empty',
        B02: 'Property border cannot be smaller than 0',
        B03: 'Property border cannot be bigger than 5',

        O01: 'Property opacity cannot be empty',
        O02: 'Property opacity cannot be smaller than 0',
        O03: 'Property opacity cannot be bigger than 1'
    };

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    function resizeCanvas() {
        var con = presenter.$view.find('.drawing').parent(),
            canvas = presenter.configuration.canvas[0];

        canvas.width = con.width();
        canvas.height = con.height();
    }

    presenter.handleImage = function (e) {
        var reader = new FileReader();
        reader.onload = function(event){
            var img = new Image();
            img.onload = function(){
                presenter.addImage(img);
            }
            img.src = event.target.result;
        }
        if(e.target.files[0]){
            reader.readAsDataURL(e.target.files[0]);
        }
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeFont(model);
        return upgradedModel;
    }

    presenter.upgradeFont = function (model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model); // Deep copy of model object

            if (upgradedModel['Font'] === undefined) {
                upgradedModel['Font'] = '';
            }

            return upgradedModel;
        };

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.$pagePanel = presenter.$view.parent().parent('.ic_page_panel');

        var upgradedModel = presenter.upgradeModel(model);
        presenter.model = upgradedModel;

        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        if (presenter.configuration.font.length === 0) presenter.configuration.font = "12px Arial";

        setDefaultAddonMode()
        presenter.configuration.pencilThickness = presenter.configuration.thickness;
        presenter.opacityByDefault = presenter.configuration.opacity;

        var $drawing = presenter.$view.find('.drawing')
        var $canvas = createCanvas()
        $drawing.append($canvas)

        var border = presenter.configuration.border;

        presenter.configuration.canvas = presenter.$view.find('canvas');
        presenter.configuration.context = presenter.configuration.canvas[0].getContext("2d");

        $(presenter.$view.find('.drawing')[0]).css('opacity', presenter.configuration.opacity);
        resizeCanvas();

        presenter.configuration.tmp_canvas = document.createElement('canvas');
        presenter.configuration.tmp_ctx = presenter.configuration.tmp_canvas.getContext('2d');
        $(presenter.configuration.tmp_canvas).addClass('tmp_canvas');
        presenter.configuration.tmp_canvas.width = presenter.configuration.canvas.width();
        presenter.configuration.tmp_canvas.height = presenter.configuration.canvas.height();

        presenter.$view.find('.drawing')[0].appendChild(presenter.configuration.tmp_canvas);

        if (presenter.configuration.border !== 0) {
            presenter.$view.find('canvas').css('border', border + 'px solid black');
        }

        if (!isPreview) {
            presenter._turnOnEventListeners();
            MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }

        presenter.setVisibility(presenter.configuration.isVisibleByDefault || isPreview);

        presenter.zoom = getZoom();
        if (presenter.zoom == "" || presenter.zoom == undefined) {
            presenter.zoom = 1;
        }
        presenter.shouldUpdateState = false;
        presenter.isModified = false;
        shouldSendModifiedEventOnLeave = false;
    };

    function createCanvas() {
        var $canvas = $(document.createElement('canvas'));
        $canvas.addClass('canvas');
        $canvas.html('element canvas is not supported by your browser');
        return $canvas
    }

    presenter.setColor = function(color) {
        presenter.shouldUpdateState = true;
        if (!isOnTextEditionMode()) {
            setDefaultAddonMode();
        }
        if (typeof color === "object") color = color[0];
        presenter.configuration.thickness = presenter.configuration.pencilThickness;
        presenter.configuration.context.globalCompositeOperation = "source-over";
        presenter.configuration.color = presenter.parseColor(color).color;
        presenter.beforeEraserColor = presenter.configuration.color;
        if (presenter.$textWrapper) {
            presenter.$textWrapper.find('textarea').css('color', presenter.configuration.color);
        }
    };

    presenter.setThickness = function(thickness) {
        presenter.shouldUpdateState = true;
        if (typeof thickness === "object") thickness = thickness[0];

        presenter.configuration.pencilThickness = presenter.parseThickness(thickness).thickness;
        if (isOnPencilMode()) {
            presenter.configuration.thickness = presenter.configuration.pencilThickness;
        }
    };

    presenter.setOpacity = function(opacity) {
        presenter.shouldUpdateState = true;
        if (typeof opacity === "object") opacity = opacity[0];

        presenter.configuration.opacity = presenter.parseOpacity(opacity).opacity;
    };

    presenter.setFont = function(font) {
        if (font !== undefined) {
            presenter.configuration.font = font;
            if (font) {
                presenter.shouldUpdateState = true;
                presenter.configuration.tmp_ctx.font = font;
                if (presenter.$textWrapper) {
                    var $textarea = presenter.$textWrapper.find('textarea');
                    var oldWidth = $textarea.css('width');
                    var oldHeight = $textarea.css('height');
                    $textarea.css('font', font);
                    $textarea.css('width', oldWidth);
                    $textarea.css('height', oldHeight);
                }
            }
        }
    };

    presenter.setEraserOff = function () {
        presenter.shouldUpdateState = true;
        setDefaultAddonMode();
        if (presenter.beforeEraserColor == undefined) {
            presenter.setColor(presenter.configuration.color);
        } else {
            presenter.setColor(presenter.beforeEraserColor);
        }
    };

    presenter.setEraserOn = function() {
        presenter.shouldUpdateState = true;
        setAddonMode(ModeEnum.eraser);

        presenter.configuration.thickness = presenter.configuration.eraserThickness;
        presenter.configuration.context.globalCompositeOperation = "destination-out";
        presenter.beforeEraserColor = presenter.configuration.color;
    };

    presenter.setEraserThickness = function(thickness) {
        presenter.shouldUpdateState = true;
        presenter.configuration.eraserThickness = presenter.parseThickness(thickness).thickness;
        if (isOnEraserMode()) {
            presenter.configuration.thickness = presenter.configuration.eraserThickness;
        }
    };

    presenter.validateModel = function(model) {

        if (ModelValidationUtils.isStringEmpty(model.Color)) {
            return getErrorObject('C01');
        }

        var parsedColor = presenter.parseColor(model.Color);
        if (!parsedColor.isValid) {
            return getErrorObject(parsedColor.errorCode);
        }

        if (ModelValidationUtils.isStringEmpty(model.Thickness)) {
            return getErrorObject('T01');
        }

        var parsedThickness = presenter.parseThickness(model.Thickness);
        if (!parsedThickness.isValid) {
            return getErrorObject(parsedThickness.errorCode);
        }

        if (ModelValidationUtils.isStringEmpty(model.Border)) {
            return getErrorObject('B01');
        }

        var parsedBorder = presenter.parseBorder(model.Border);
        if (!parsedBorder.isValid) {
            return getErrorObject(parsedBorder.errorCode);
        }

        if (ModelValidationUtils.isStringEmpty(model.Opacity)) {
            return getErrorObject('O01');
        }

        var parsedOpacity = presenter.parseOpacity(model.Opacity);
        if (!parsedOpacity.isValid) {
            return getErrorObject(parsedOpacity.errorCode);
        }

        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        return {
            addonID: model.ID,
            color: parsedColor.color,
            thickness: parsedThickness.thickness,
            border: parsedBorder.border,
            opacity: parsedOpacity.opacity,

            canvas: null,
            context: null,

            width: model.Width,
            height: model.Height,
            isValid: true,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isExerciseStarted: false,
            font: model.Font
        };
    };

    presenter.parseColor = function(color) {

        if (color[0] === '#' &amp;&amp; !(color.length === 7)) {
            return getErrorObject('C02');
        }

        if (color[0] !== '#') {
            color = presenter.colourNameToHex(color);
        }

        if (!color) {
            return getErrorObject('C03');
        }

        return {
            color: color,
            isValid: true
        };

    };

    presenter.parseThickness = function(thickness) {

        if (thickness &lt; 1) {
            return getErrorObject('T02');
        }

        if (thickness &gt; 40) {
            return getErrorObject('T03');
        }

        return {
            thickness: thickness,
            isValid: true
        };

    };

    presenter.parseBorder = function(border) {

        if (border &lt; 0) {
            return getErrorObject('B02');
        }

        if (border &gt; 5) {
            return getErrorObject('B03');
        }

        return {
            border: border,
            isValid: true
        };

    };

    presenter.parseOpacity = function(opacity) {
        if (opacity &lt; 0) {
            return getErrorObject('O02');
        }

        if (opacity &gt; 1) {
            return getErrorObject('O03');
        }

        return {
            opacity: opacity,
            isValid: true
        };
    };

    function setColorToBackgroundTmpCanvas (color) {
        var tmp_ctx = presenter.configuration.tmp_ctx;
        var tmp_canvas = presenter.configuration.tmp_canvas;
        tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        tmp_ctx.fillStyle = color;
        tmp_ctx.fillRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        tmp_ctx.drawImage(presenter.$view.find("canvas")[0], 0, 0);
    }

    presenter.handleDownloadImage = function () {
        var fillStyle = presenter.configuration.tmp_ctx.fillStyle;
        setColorToBackgroundTmpCanvas('white');

        var canvas = presenter.$view.find("canvas.tmp_canvas")[0], data = canvas.toDataURL("image/png");
        data = data.replace(/^data:image\/[^;]*/, 'data:application/octet-stream');
        data = data.replace(/^data:application\/octet-stream/, 'data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=Board.png');
        this.href = data;

        tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
        tmp_ctx.fillStyle = fillStyle;
    };

    presenter.downloadBoard = function () {
        
        var element = document.createElement("a");
        element.setAttribute("id", "dl");
        element.setAttribute("download", "Board.png");
        element.setAttribute("href", "#");
        element.onclick = presenter.handleDownloadImage;
        element.click();
    }

    presenter.executeCommand = function (name, params) {
        if (!presenter.configuration.isValid) {
            return;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'setThickness': presenter.setThickness,
            'setColor': presenter.setColor,
            'setOpacity': presenter.setOpacity,
            'setEraserOn': presenter.setEraserOn,
            'setEraserOff': presenter.setEraserOff,
            'setEraserThickness': presenter.setEraserThickness,
            'addText': presenter.addText,
            'setFont': presenter.setFont,
            'uploadImage': presenter.uploadImage,
            'downloadBoard': presenter.downloadBoard
        };

        addImageToCanvasIfOnImageEditionMode();
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.uploadImage = function() {
        var element = document.createElement("input");
        $('body')[0].append(element);
        element.style.display = 'none';
        element.setAttribute("id", "importFile");
        element.setAttribute("type", "file");
        element.setAttribute("accept", "image/png,image/jpeg");

        if (presenter.imageInputElement) presenter.imageInputElement.remove();
        presenter.imageInputElement = element;

        element.addEventListener('change', presenter.handleImage);
        element.click();
    }

    presenter.reset = function() {
        presenter.configuration.context.clearRect(0, 0, presenter.configuration.canvas[0].width, presenter.configuration.canvas[0].height);
        sendValueChangedEmptyEvent();

        presenter.setColor(presenter.model.Color);
        presenter.setThickness(presenter.model.Thickness);
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.configuration.opacity = presenter.opacityByDefault;
        presenter.beforeEraserColor = presenter.configuration.color;

        setOverflowWorkAround(true);
        setOverflowWorkAround(false);

        presenter.shouldUpdateState = false;
        presenter.isModified = false;
        shouldSendModifiedEventOnLeave = false;
        shouldSkipNextModifiedEvent = false;
    };

    presenter.getState = function() {
        if (!presenter.shouldUpdateState) {
            return;
        }

        addImageToCanvasIfOnImageEditionMode();

        var addonMode = presenter.configuration.addonMode,
            color = presenter.configuration.color,
            pencilThickness = presenter.configuration.pencilThickness,
            eraserThickness = presenter.configuration.eraserThickness,
            c = presenter.$view.find("canvas")[0],
            data = c.toDataURL("image/png"),
            font = presenter.configuration.font;

        const textEditorResult = presenter.createTextEditorResult();

        return JSON.stringify({
            addonMode: addonMode,
            color: color,
            pencilThickness: pencilThickness,
            eraserThickness: eraserThickness,
            data: data,
            isVisible: presenter.configuration.isVisible,
            opacity: presenter.configuration.opacity,
            font: font,
            textEditorResult: textEditorResult ? textEditorResult.getState() : null,
            isModified: presenter.isModified
        });
    };

    presenter.upgradeStateForOpacity = function (parsedState) {
        if (parsedState.opacity == undefined) {
            parsedState.opacity = 0.9;
        }

        return parsedState;
    };

    presenter.upgradeStateForImageAndText = function (parsedState) {
        if (parsedState.font == undefined) {
            parsedState.font = "";
        }

        if (parsedState.addonMode == undefined) {
            if (parsedState.isPencil !== undefined &amp;&amp; !parsedState.isPencil) {
                parsedState.addonMode = "eraser";
            } else {
                parsedState.addonMode = "pencil";
            }
        }

        return parsedState;
    }

    presenter.upgradeTextEditorResult = function (parsedState) {
        if (parsedState.textEditorResult == undefined) {
            parsedState.textEditorResult = null;
        }

        return parsedState;
    }

    presenter.upgradeState = function (parsedState) {
        var upgradedState = presenter.upgradeStateForOpacity(parsedState);
        upgradedState = presenter.upgradeStateForImageAndText(upgradedState);
        upgradedState = presenter.upgradeTextEditorResult(upgradedState);
        return upgradedState;
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsedState = JSON.parse(state);

        parsedState = presenter.upgradeState(parsedState);

        var data = parsedState.data,
            addonMode = parsedState.addonMode,
            color = parsedState.color,
            savedImg = new Image();

        savedImg.onload = function() {
            presenter.configuration.context.drawImage(savedImg, 0, 0);
        };
        savedImg.src = data;

        presenter.configuration.pencilThickness = parsedState.pencilThickness;
        presenter.configuration.eraserThickness = parsedState.eraserThickness;
        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.shouldUpdateState = true;
        presenter.configuration.opacity = parsedState.opacity;
        presenter.configuration.addonMode = addonMode;
        const wasTextEditionMode = addonMode === ModeEnum.textEdition;
        if (wasTextEditionMode) {
            setDefaultAddonMode();
        }
        if (isOnPencilMode()) {
            presenter.setColor(color);
        } else {
            presenter.configuration.thickness = presenter.configuration.eraserThickness;
            presenter.configuration.color = "rgba(0, 0, 0, 1)";
        }
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.beforeEraserColor = color;
        presenter.setFont(parsedState.font);
        if (wasTextEditionMode) {
            if (parsedState.textEditorResult) {
                const textEditorResult = presenter.buildTextEditorResult(
                    parsedState.textEditorResult.brokenText, parsedState.textEditorResult.lineHeight,
                    parsedState.textEditorResult.x, parsedState.textEditorResult.y
                );
                textEditorResult.embed();
            }
        }
        if (parsedState.hasOwnProperty("isModified")) {
            presenter.isModified = parsedState.isModified;
        } else {
            presenter.isModified = true;
        }
    };

    presenter.addText = function() {
        if (!isOnTextEditionMode()) {
            presenter.setEraserOff();
            setAddonMode(ModeEnum.textEdition);
            presenter.displayTextFieldPopup();
        } else {
            presenter.closeTextFieldPopup();
        }
    }

    presenter.displayTextFieldPopup = function() {
        if (presenter.$textWrapper != null) return;
        var $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        var textareaWidth = 100;
        $wrapper.css('position', 'absolute');
        $wrapper.css('left', Math.round(presenter.configuration.canvas.width()/2 - textareaWidth/2) + 'px');
        $wrapper.css('top', '45%');
        $wrapper.css('padding', '0px');

        var $textHandle = $('&lt;div&gt;&lt;/div&gt;');
        $textHandle.addClass('text-handle');
        $textHandle.css('display', 'block');
        $textHandle.css('width', '14px');
        $textHandle.css('height', '14px');
        $textHandle.css('position', 'absolute');
        $textHandle.css('left', '-13px');
        $textHandle.css('top', '-13px');
        $textHandle.css('background-color', 'rgb(133,133,133)');
        $textHandle.css('cursor', 'pointer');
        $wrapper.append($textHandle);

        var $closeButton = $('&lt;div&gt;x&lt;/div&gt;');
        $closeButton.addClass('text-close');
        $closeButton.css('display', 'block');
        $closeButton.css('color', 'rgb(133,133,133)');
        $closeButton.css('font-size', '16px');
        $closeButton.css('width', '0.6em');
        $closeButton.css('height', '1em');
        $closeButton.css('position', 'absolute');
        $closeButton.css('right', '0px');
        $closeButton.css('top', '-1.2em');
        $closeButton.css('cursor', 'pointer');
        $closeButton.click(presenter.closeTextFieldPopup);
        $wrapper.append($closeButton);

        var $textarea = $('&lt;textarea&gt;&lt;/textarea&gt;');
        $textarea.css('font',presenter.configuration.font);
        $textarea.css('color',presenter.configuration.color);
        $textarea.css('padding','0px');
        $textarea.css('background-color','#0000');
        $textarea.css('width',textareaWidth + 'px');
        $textarea.on('input', function() {
            if ($textarea[0].scrollHeight &gt; $textarea[0].clientHeight) {
                $textarea.css('height', $textarea[0].scrollHeight+'px');
            }
        });
        $wrapper.append($textarea);

        presenter.$view.append($wrapper);
        presenter.$textWrapper = $wrapper;
        var scale = getScale();
        $wrapper.draggable({
            handle: ".text-handle",
            containment: "parent",
            drag: function( event, ui ) {
                ui.position.left = ui.position.left / scale.X;
                ui.position.top = ui.position.top / scale.Y;
              }
        });
        $textarea.focus();
    };

    presenter.closeTextFieldPopup = function() {
        if (presenter.$textWrapper != null) {
            presenter.$textWrapper.remove();
            presenter.$textWrapper = null;
            setDefaultAddonMode();
        }
    }

    presenter.finishEditTextMode = function() {
        if (presenter.$textWrapper == null) {
            return;
        }

        const textEditorResult = presenter.createTextEditorResult();
        if (textEditorResult) {
            textEditorResult.embed();
        }

        presenter.closeTextFieldPopup();
    }

    presenter.getLineHeight = function($textarea, lineNumber) {
        var $testArea = $textarea.clone(true);
        $testArea.attr('visibility', 'hidden');
        $textarea.after($testArea);
        $testArea.css('height', '0px');
        var lineHeight = $testArea[0].scrollHeight / lineNumber;
        $testArea.remove();
        return lineHeight;
    }

    presenter.getBrokenText = function($textarea) {
        var $testArea = $textarea.clone(true);
        $testArea.attr('visibility', 'hidden');
        $testArea.attr('wrap', 'off');
        $textarea.after($testArea);
        $testArea.val('');
        var emptyWidth = $testArea[0].scrollWidth;

        var srcTextArray = $textarea.val().split(/\n/);

        function isBrokenText(text) {
            // If a scroll would show up, that means that the text is too long
            // and would end up broken into multiple lines
            $testArea.val(text);
            var result = emptyWidth &lt; $testArea[0].scrollWidth;
            $testArea.val('');
            return result;
        }

        var resultArray = [];

        for (var i = 0; i &lt; srcTextArray.length; i++) {
            var srcText = srcTextArray[i];
            if (srcText.length == 0) {
                // save an empty line and move on
                resultArray.push(srcText);
                continue;
            }
            var wordArray = srcText.match(/[^\s][^\s]*|\s/g); // split the current line we're working on into individual words and whitespaces
            var workString = "";
            for (var j = 0; j &lt; wordArray.length; j++) {
                if (j &gt; 0 &amp;&amp; /^\s$/.test(wordArray[j]) &amp;&amp; workString.length == 0) {
                    // if a line has been broken due to being too long, the new row should not start with whitespaces
                    continue;
                }
                if (isBrokenText(workString + wordArray[j])) {
                    if (workString.length == 0) {
                        // In this case a single word is too long to fit and needs to be broken into lines
                        var brokenString = "";
                        for (var k = 0; k &lt; wordArray[j].length; k++) {
                            var newLetter = wordArray[j][k];
                            if (isBrokenText(brokenString + newLetter)) {
                                resultArray.push(brokenString);
                                brokenString = newLetter;
                            } else {
                                brokenString += newLetter;
                            }
                        }
                        workString += brokenString;
                    } else {
                        // the new word would cause the work string to exceed line length
                        resultArray.push(workString);
                        if (/^\s$/.test(wordArray[j])) {
                            workString = "";
                        } else {
                            workString = wordArray[j];
                        }
                    }
                } else {
                    workString += wordArray[j];
                }
            }
            if (workString.length &gt; 0) resultArray.push(workString);
        }
        $testArea.remove();
        return resultArray;
    }

    function TextEditorResult (brokenText, lineHeight, x, y) {
        this.brokenText = brokenText;
        this.lineHeight = lineHeight;
        this.x = x;
        this.y = y;
    }

    TextEditorResult.prototype = Object.create(Object.prototype);
    TextEditorResult.prototype.constructor = TextEditorResult;

    TextEditorResult.prototype.embed = function () {
        presenter.embedText(this.brokenText, this.x, this.y, this.lineHeight);
    };

    TextEditorResult.prototype.getState = function () {
        return {
            brokenText: this.brokenText,
            lineHeight: this.lineHeight,
            x: this.x,
            y: this.y,
        };
    };

    presenter.buildTextEditorResult = function (brokenText, lineHeight, x, y) {
        return new TextEditorResult(brokenText, lineHeight, x, y);
    };

    presenter.createTextEditorResult = function() {
        if (presenter.$textWrapper == null) {
            return;
        }
        const $textarea = presenter.$textWrapper.find("textarea");
        const textArray = presenter.getBrokenText($textarea);
        if (textArray.length === 0) {
            return;
        }

        presenter.shouldUpdateState = true;
        presenter.isModified = true;
        sendModifiedEvent();
        shouldSkipNextModifiedEvent = true;

        const drawingWrapper = presenter.$view.find(".drawing")[0];
        const x = presenter.$textWrapper[0].offsetLeft - drawingWrapper.offsetLeft + 1;
        const y = presenter.$textWrapper[0].offsetTop - drawingWrapper.offsetTop - 1;
        const lineHeight = presenter.getLineHeight($textarea, textArray.length);
        return presenter.buildTextEditorResult(textArray, lineHeight, x, y);
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        presenter.configuration.tmp_canvas.removeEventListener("click", presenter.onTmpCanvasClick, false);
        document.removeEventListener("keydown", presenter.removeImage, false);

        if (presenter.imageInputElement) presenter.imageInputElement.remove();
        presenter.closeTextFieldPopup();
    };

    function sendModifiedEvent() {
        if (shouldSkipNextModifiedEvent) {
            shouldSkipNextModifiedEvent = false;
            return;
        }

        if (!presenter.isModified) {
            return;
        }
        presenter.sendModifiedEvent();
    }

    presenter.sendModifiedEvent = function () {
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'modified',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    function sendPreDestroyedEmptyEvent() {
        presenter.sendEmptyEvent(false);
    }

    function sendValueChangedEmptyEvent() {
        presenter.sendEmptyEvent(true);
    }

    presenter.sendEmptyEvent = function (isValueChangedEvent) {
        const eventName = isValueChangedEvent ? 'ValueChanged' : 'PreDestroyed';
        const eventData = {
            'source': presenter.configuration.addonID,
            'item': '',
            'value': 'empty',
        };
        if (isValueChangedEvent) {
            eventData.score = '';
        }

        eventBus.sendEvent(eventName, eventData);
    };

    presenter.preDestroy = function() {
        if (!presenter.isModified) {
            sendPreDestroyedEmptyEvent();
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="EdgeAnimation" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Animations" nameLabel="EdgeAnimation_property_animations" type="list">
			<property displayName="Composition Class" name="compositionClass" nameLabel="EdgeAnimation_property_composition_class" type="string"/>
			<property displayName="edge.js" name="edgeFile" nameLabel="EdgeAnimation_property_edge_file" type="file"/>
			<property displayName="edgeActions.js" name="edgeActionsFile" nameLabel="EdgeAnimation_property_edge_actions_file" type="file"/>
		</property>
		<property name="Images" nameLabel="EdgeAnimation_property_images" type="list">
			<property displayName="Animation Item Number" name="animationItem" nameLabel="EdgeAnimation_property_animation_item" type="string"/>
			<property displayName="Edge element IDs (csv)" name="elementId" nameLabel="EdgeAnimation_property_element_id" type="string"/>
			<property displayName="Image File" name="imageFile" nameLabel="EdgeAnimation_property_image_file" type="image"/>
		</property>
		<property displayName="Initial Animation" name="initialAnimation" nameLabel="EdgeAnimation_property_initial_animation" type="string"/>
		<property name="Narration" nameLabel="EdgeAnimation_property_narration" type="narration"/>
	</model>
<css>.edgeMultiAnimWrapper{
    width: 100%;
    height: 100%;
}

.Stage{
    position: absolute;
}

.edge-loading-image{
    position: absolute;
    width: 30px;
    height: 30px;
    border: 0;
}
</css><view>&lt;div class="edgeMultiAnimWrapper"&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="edge-loading-image"/&gt;
</view><preview>&lt;div class="edgeMultiAnimWrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonEdgeAnimation_create(){
    var currentAnimationItem = 1;

    var presenter = function(){};
    var onDocLoaded = null;
    var Animations;
    
    presenter.setStateStatus = false;
    presenter.commentAlreadyLoaded = false;
    presenter.$view = null;

    presenter.run = function(view, model){
        Animations = model.Animations;
        presenter.Animations = model.Animations;
        presenter.addonID = model.ID;
        presenter.$view = $(view);
        presenter.model = model;
        presenter.autoplayState = undefined;
        
		presenter.isVisible = model["Is Visible"] == 'True';
		presenter.setVisibility(presenter.isVisible);
		
		//show loading icon
		var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
		var loadingIconImg = $(view).find('.edge-loading-image')[0];
		$(loadingIconImg).css({
            left: (presenter.model.Width/2)-15 + 'px',
            top: (presenter.model.Height/2)-15 + 'px'
        });
		$(loadingIconImg).attr('src',loadingSrc);
		
		$(model.Animations).each(function(i, animation){
			if(animation.compositionClass === '' &amp;&amp; animation.edgeFile === '' &amp;&amp; animation.edgeActionsFile === ''){
				presenter.errorHandler("This addon needs all files in Animations list to work");
				return;
			}
		});
		
        presenter.preloadImages(model.Images, function() {
            presenter.presenterLogic(view, model);
        });
    };

    presenter.createPreview = function (view, model) {
		//presenter.run(view,model);
    };
	
	presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };
	
	presenter.errorHandler = function(message){
		presenter.$view.prepend('&lt;div style="color: red; font-size: 12px; font-family: Arial;"&gt;'+message+'&lt;/span&gt;');
	};
	
    presenter.preloadImages = function(images, callback){
        var count = images.length;
        if(count === 1 &amp;&amp; images[0].imageFile === '') {
            callback();
        }
        var loaded = 0;
        $(images).each(function() {
            $('&lt;img&gt;').attr('src', this.imageFile).load(function() {
                loaded++;
                if (loaded === count) {
                    callback();
                }
            });
        });
    };

    presenter.loadMediaToAnimation = function(model){
        var CompositionClass;
        var elementTagName;
		//IMAGES
		if(model.Images.length === 1 &amp;&amp; model.Images[0].imageFile === '') {
            return;
        }
        $(model.Images).each(function(index1, value){
            var animItemsArray = value.animationItem.split(',');
            var idsArray = value.elementId.split(',');

            $(animItemsArray).each(function(index2, item){

                $(idsArray).each(function(index3, id){
                    CompositionClass = model.Animations[item - 1].compositionClass;

                    elementTagName = $("#Stage" + model.ID + CompositionClass + "_" + id).prop("tagName");
                    if(elementTagName == 'DIV'){
                        $("#Stage" + model.ID + CompositionClass + "_" + id).css("background-image", "url(" + value.imageFile + ")");
                    } else if(elementTagName=='IMG') {
                        $("#Stage" + model.ID + CompositionClass + "_" + id).attr('src', value.imageFile);
                    }
                });

            });
        });
    };

    presenter.presenterLogic = function(view, model){
        if(model.initialAnimation === ""){
            model.initialAnimation = 0;
        } else {
            model.initialAnimation = parseInt(model.initialAnimation, 10) - 1;
        }

        $(model.Animations).each(function(i, animation){
            var display = "none";
            if(currentAnimationItem - 1 == i){
                display = "block";
            }

            $('head').append('&lt;script&gt;    window.AdobeEdge = window.AdobeEdge || {};    window.AdobeEdge.bootstrapLoading = true; &lt;/script&gt;');
            $('head').append('&lt;!--Adobe Edge Runtime--&gt;');
            var headPrependStyle = '&lt;style&gt;.edgeLoad-' + animation.compositionClass + ' { visibility:hidden; }&lt;/style&gt;';
            $('head').append(headPrependStyle);
            $('head').append('&lt;!--Adobe Edge Runtime End--&gt;');

            var stage = $('&lt;div&gt;&lt;/div&gt;');
            var edgeAnimWrapper = $(view).find('.edgeMultiAnimWrapper')[0];

            stage.attr('id', 'Stage' + model.ID + animation.compositionClass);
            stage.addClass(animation.compositionClass);
            stage.css('display', display);
            stage.addClass('Stage');
            $(edgeAnimWrapper).prepend(stage);
				
            run(animation);
        });

    };

    function run(animation){
        (function(compId){
            window.AdobeEdge = window.AdobeEdge || {};
            window.AdobeEdge.bootstrapLoading = true;
            // Include yepnope
            if(!AdobeEdge.yepnope) {
                /*yepnope1.5.x|WTFPL*/
                (function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&amp;&amp;g(l.readyState)&amp;&amp;(u.r=o=1,!q&amp;&amp;h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&amp;&amp;m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&amp;&amp;y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&amp;&amp;(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&amp;&amp;(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&amp;&amp;h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&amp;&amp;!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&amp;&amp;"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&amp;&amp;!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&amp;&amp;(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f&lt;d;f++)g=a[f].split("="),(e=z[g.shift()])&amp;&amp;(c=e(c,g));for(f=0;f&lt;b;f++)c=x[f](c);return c}function g(a,e,f,g,h){var i=b(a),j=i.autoCallback;i.url.split(".").pop().split("?").shift(),i.bypass||(e&amp;&amp;(e=d(e)?e:e[a]||e[g]||e[a.split("/").pop().split("?")[0]]),i.instead?i.instead(a,e,f,g,h):(y[i.url]?i.noexec=!0:y[i.url]=1,f.load(i.url,i.forceCSS||!i.forceJS&amp;&amp;"css"==i.url.split(".").pop().split("?").shift()?"c":c,i.noexec,i.attrs,i.timeout),(d(e)||d(j))&amp;&amp;f.load(function(){k(),e&amp;&amp;e(i.origUrl,h,g),j&amp;&amp;j(i.origUrl,h,g),y[i.url]=2})))}function h(a,b){function c(a,c){if(a){if(e(a))c||(j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}),g(a,j,b,0,h);else if(Object(a)===a)for(n in m=function(){var b=0,c;for(c in a)a.hasOwnProperty(c)&amp;&amp;b++;return b}(),a)a.hasOwnProperty(n)&amp;&amp;(!c&amp;&amp;!--m&amp;&amp;(d(j)?j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}:j[n]=function(a){return function(){var b=[].slice.call(arguments);a&amp;&amp;a.apply(this,b),l()}}(k[n])),g(a[n],j,b,n,h))}else!c&amp;&amp;l()}var h=!!a.test,i=a.load||a.both,j=a.callback||f,k=j,l=a.complete||f,m,n;c(h?a.yep:a.nope,!!i),i&amp;&amp;c(i)}var i,j,l=this.yepnope.loader;if(e(a))g(a,0,l,0);else if(w(a))for(i=0;i&lt;a.length;i++)j=a[i],e(j)?g(j,0,l,0):w(j)?B(j):Object(j)===j&amp;&amp;h(j,l);else Object(a)===a&amp;&amp;h(a,l)},B.addPrefix=function(a,b){z[a]=b},B.addFilter=function(a){x.push(a)},B.errorTimeout=1e4,null==b.readyState&amp;&amp;b.addEventListener&amp;&amp;(b.readyState="loading",b.addEventListener("DOMContentLoaded",A=function(){b.removeEventListener("DOMContentLoaded",A,0),b.readyState="complete"},0)),a.yepnope=k(),a.yepnope.executeStack=h,a.yepnope.injectJs=function(a,c,d,e,i,j){var k=b.createElement("script"),l,o,e=e||B.errorTimeout;k.src=a;for(o in d)k.setAttribute(o,d[o]);c=j?h:c||f,k.onreadystatechange=k.onload=function(){!l&amp;&amp;g(k.readyState)&amp;&amp;(l=1,c(),k.onload=k.onreadystatechange=null)},m(function(){l||(l=1,c(1))},e),i?k.onload():n.parentNode.insertBefore(k,n)},a.yepnope.injectCss=function(a,c,d,e,g,i){var e=b.createElement("link"),j,c=i?h:c||f;e.href=a,e.rel="stylesheet",e.type="text/css";for(j in d)e.setAttribute(j,d[j]);g||(n.parentNode.insertBefore(e,n),m(c,0))}})(this,document);AdobeEdge.yepnope = window.yepnope;
            }
            // end yepnope


            var htFallbacks;
            var testEle=document.createElement("div");function isSupported(a){var d=testEle.style,e;for(i=0;i&lt;a.length;i++)if(e=a[i],d[e]!==void 0)return!0;return!1}function supportsRGBA(){testEle.cssText="background-color:rgba(150,255,150,.5)";if((""+testEle.style.backgroundColor).indexOf("rgba")==0)return!0;return!1}
            var hasTransform=isSupported(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"]),hasSVG=!!document.createElementNS&amp;&amp;!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,hasRGBA=supportsRGBA(),hasJSON=window.JSON&amp;&amp;window.JSON.parse&amp;&amp;window.JSON.stringify,readyToPlay=!1;function safeColor(a){a=""+a;if(!hasRGBA&amp;&amp;a.indexOf("rgba")==0){var d=a.lastIndexOf(",");d&gt;0&amp;&amp;(a="rgb("+a.substring(5,d)+")")}return a}
            AdobeEdge._preloaders=AdobeEdge._preloaders||[];AdobeEdge._preloaders.push(function(){filesToLoad&amp;&amp;(loadResources(filesToLoad),filesToLoad=void 0)});function doLoadResources(){for(var a=0;a&lt;AdobeEdge._preloaders.length;a++)AdobeEdge._preloaders[a]()}AdobeEdge._readyplayers=AdobeEdge._readyplayers||[];AdobeEdge._readyplayers.push(function(){readyToPlay&amp;&amp;AdobeEdge.okToLaunchComposition(compId)});
            function playWhenReady(){AdobeEdge._playWhenReady=!0;for(var a=0;a&lt;AdobeEdge._readyplayers.length;a++)AdobeEdge._readyplayers[a]()}function edgeCallback(a){htFallbacks[a]&amp;&amp;(a=htFallbacks[a]);AdobeEdge.preload.got[a]=!0;if(a==AdobeEdge.preload.last)!AdobeEdge.bootstrapLoading||AdobeEdge._playWhenReady?AdobeEdge.okToLaunchComposition(compId):readyToPlay=!0,AdobeEdge.preload.busy=!1,AdobeEdge.preload.q.length&gt;0&amp;&amp;(a=AdobeEdge.preload.q.pop(),AdobeEdge.requestResources(a.files,a.callback))}
            AdobeEdge.requestResources=AdobeEdge.requestResources||function(a,d){AdobeEdge.yepnope.errorTimeout=4E3;AdobeEdge.preload.busy=!0;AdobeEdge.preload.got={};var e,b=a.length,h=[],c;for(e=0;e&lt;b;e++){c=a[e];if(typeof c==="string")url=c,c={load:url};else if(url=c.yep||c.load,c.callback){var k=c.callback;c.callback=function(a,b,c){k(a,b,c)&amp;&amp;d(a,b,c)}}if(!c.callback)c.callback=d;if(!AdobeEdge.preload.got[url])h.push(c),AdobeEdge.preload.last=url}h.length&amp;&amp;AdobeEdge.yepnope(h)};
            var filesToLoad,dlContent,preContent,doDelayLoad,signaledLoading,loadingEvt,requiresSVG,htLookup={},aLoader,aEffectors;function loadResources(a,d){AdobeEdge.preload=AdobeEdge.preload||[];AdobeEdge.preload.q=AdobeEdge.preload.q||[];d||!isCapable()?filesToLoad=a:AdobeEdge.preload.busy?AdobeEdge.preload.q.push({files:a,callback:edgeCallback}):AdobeEdge.requestResources(a,edgeCallback);}
            function splitUnits(a){var d={};d.num=parseFloat(a);if(typeof a=="string")d.units=a.match(/[a-zA-Z%]+$/);if(d.units&amp;&amp;typeof d.units=="object")d.units=d.units[0];return d}function defaultUnits(a){var d=a;if(a!=="auto"&amp;&amp;(a=splitUnits(a),!a||!a.units))d+="px";return d}function findNWC(a,d){if(String(a.className).indexOf(d)!=-1)return a;for(var e=a.childNodes,b=0;b&lt;e.length;b++){var h=findNWC(e[b],d);if(h!=!1)return h}return!1}
            function simpleContent(a,d,e){var b=document.getElementsByTagName("body")[0],e=e||findNWC(b,compId),h,c,k,g;if(e){if(e.style.position!="absolute"&amp;&amp;e.style.position!="relative")e.style.position="relative"}else e=b;for(var m=0;m&lt;a.length;m++){b=a[m];b.type=="image"?(h=document.createElement("img"),h.src=b.fill[1]):h=document.createElement("div");h.id=b.id;g=h.style;if(b.type=="text"){if(c=b.font){if(c[0]&amp;&amp;c[0]!=="")g.fontFamily=c[0];typeof c[1]!="object"&amp;&amp;(c[1]=[c[1]]);c[1][1]||(c[1][1]="px");if(c[1][0]&amp;&amp;
                c[1][0]!=="")g.fontSize=c[1][0]+c[1][1];if(c[2]&amp;&amp;c[2]!=="")g.color=safeColor(c[2]);if(c[3]&amp;&amp;c[3]!=="")g.fontWeight=c[3];if(c[4]&amp;&amp;c[4]!=="")g.textDecoration=b.font[4];if(c[5]&amp;&amp;c[5]!=="")g.fontStyle=b.font[5]}if(b.align&amp;&amp;b.align!="auto")g.textAlign=b.align;if(b.position)g.position=b.position;if((!b.rect[2]||b.rect[2]&lt;=0)&amp;&amp;(!b.rect[3]||b.rect[3]&lt;=0))g.whiteSpace="nowrap";h.innerHTML=window.xssUtils.sanitize(b.text)}if(d)h.className=d;g.position="absolute";c=b.rect[0];k=b.rect[1];if(b.transform&amp;&amp;b.transform[0]){var j=b.transform[0][0],
                f=splitUnits(j);if(f&amp;&amp;f.units&amp;&amp;(j=f.num,f.units=="%"&amp;&amp;b.rect[2])){var f=b.rect[2],l=splitUnits(b.rect[2]);if(l&amp;&amp;l.units)f=l.num,l.units=="%"&amp;&amp;(f=f/100*e.offsetWidth);j=j/100*f;e.offsetWidth&gt;0&amp;&amp;(j=j/e.offsetWidth*100)}if(f=splitUnits(c))c=f.num;c+=j;if(!f.units)f.units="px";c+=f.units;if(b.transform[0].length&gt;1){j=b.transform[0][1];if((f=splitUnits(j))&amp;&amp;f.units)if(j=f.num,f.units=="%"&amp;&amp;b.rect[3]){f=b.rect[3];if((l=splitUnits(b.rect[3]))&amp;&amp;l.units)f=l.num,l.units=="%"&amp;&amp;(f=f/100*e.offsetHeight);j=j/100*
                f;e.offsetHeight&gt;0&amp;&amp;(j=j/e.offsetHeight*100)}if(f=splitUnits(k))k=f.num;k+=j;if(!f.units)f.units="px";k+=f.units}}g.left=defaultUnits(c);g.top=defaultUnits(k);g.width=defaultUnits(b.rect[2]);g.height=defaultUnits(b.rect[3]);if(b.linkURL)htLookup[h.id]=b,h.onclick=function(){var a=htLookup[this.id];a.linkTarget?window.open(a.linkURL,a.linkTarget):window.location.href=a.linkURL},g.cursor="pointer";e.appendChild(h);if(b.c)for(g=0;g&lt;b.c.length;g++)simpleContent(b.c[g],d,h)}}
            var fnCycle=function(a){a?fnCycle&amp;&amp;setTimeout(fnCycle,20):a={event:"loading",progress:0};loadingEvt&amp;&amp;loadingEvt(a)},aBootcompsLoaded=[];if(!window.AdobeEdge.bootstrapListeners)window.AdobeEdge.bootstrapListeners=[];window.AdobeEdge.bootstrapCallback=function(a){window.AdobeEdge.bootstrapListeners.push(a);if(aBootcompsLoaded.length&gt;0)for(var d=0;d&lt;aBootcompsLoaded.length;d++)a(aBootcompsLoaded[d])};if(!window.AdobeEdge.preloadComplete)window.AdobeEdge.preloadComplete={};
		//window.AdobeEdge.preloadComplete
            window.AdobeEdge.preloadComplete[compId]=function(a){
				presenter.loadMediaToAnimation(presenter.model);
                AdobeEdge.$_(".edgePreload"+a).css("display","none");
				fnCycle=null;loadingEvt&amp;&amp;loadingEvt({event:"done",progress:1,reason:"complete"});aBootcompsLoaded.push(a);for(var d=window.AdobeEdge.bootstrapListeners.length,e=0;e&lt;d;e++)try{window.AdobeEdge.bootstrapListeners[e](a)}catch(b){console.log("bootstrap error "+b)}
				//hide loading icon
				var loadingIconImg = presenter.$view.find('.edge-loading-image')[0];
				$(loadingIconImg).css('display','none');
			};
			function isCapable(){if(hasTransform){if(requiresSVG&amp;&amp;!hasSVG)return!1;return!0}return!1}
            onDocLoaded = function(){window.AdobeEdge.loaded=!0;fnCycle({event:"begin"});isCapable()?(preContent&amp;&amp;preContent.dom&amp;&amp;simpleContent(preContent.dom,"edgePreload"+compId),filesToLoad&amp;&amp;!signaledLoading&amp;&amp;(loadResources(filesToLoad),filesToLoad=void 0)):dlContent&amp;&amp;dlContent.dom&amp;&amp;(loadingEvt&amp;&amp;loadingEvt({event:"done",progress:1,reason:"downlevel"}),simpleContent(dlContent.dom))};
            window.AdobeEdge = window.AdobeEdge || {};
            window.AdobeEdge.framework = 'jquery';

            onDocLoaded();

            requiresSVG=false;

            doDelayLoad=false;
            htFallbacks={
            };

            var edgeRuntimePath = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "addons/resources/edge2.0.1.268.js");

            aLoader = [
                { load: edgeRuntimePath},
                { load: animation.edgeFile},
                { load: animation.edgeActionsFile}
            ];

            if (AdobeEdge.bootstrapLoading){
                signaledLoading = true;
                AdobeEdge.loadResources=doLoadResources;
                AdobeEdge.playWhenReady=playWhenReady;
            }

            loadResources(aLoader, doDelayLoad);
				
            preContent={
                dom: [
                ]};//simpleContent

            dlContent={dom: [ ]};//simpleContent

            AdobeEdge.loadResources();
			AdobeEdge.playWhenReady();
			
			AdobeEdge.bootstrapCallback(function(compId) {
				//necessary for situation with identical animations on succeeding pages
				$.each(AdobeEdge.compositionDefns, function() {
					this.launched = false;
				});
				presenter.setAutoplay(compId);//necessary for handling reset
			});
			
        })(animation.compositionClass);
    }
	
	presenter.setVisibility = function(isVisible) {
		presenter.isVisible = isVisible;
		presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
	};
	
	presenter.show = function() {
		presenter.setVisibility(true);
	};
	
	presenter.hide = function() {
		presenter.setVisibility(false);
	};	
	
	presenter.setAutoplay = function(compId){
		var myAnim = AdobeEdge.getComposition(compId),
		mySymbol = myAnim.getStage();
		presenter.autoplayState = mySymbol.timelines["Default Timeline"].autoPlay;
	};

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'switchAnimation': presenter.switchAnimation,
            'getEdgeComposition' : presenter.getEdgeComposition,
            'getEdgeStage' : presenter.getEdgeStage,
            'stop': presenter.stop,
            'play': presenter.play,
            'pause' : presenter.pause
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.switchAnimation = function(item){
        currentAnimationItem = item;
        $(presenter.Animations).each(function(i,v){
            if(item == (i+1)){
                $('#Stage' + presenter.addonID + v.compositionClass).css("display", "block");
            }else{
                $('#Stage' + presenter.addonID + v.compositionClass).css("display", "none");
            }
        });
    };

    presenter.pause = function(position){
        var currCompClass = presenter.Animations[currentAnimationItem - 1].compositionClass,
            myAnim = AdobeEdge.getComposition(currCompClass),
            mySymbol = myAnim.getSymbols("stage")[0],
            currentPosition = position != undefined ? position[0] : mySymbol.getPosition();

		//stop "stage" and all its children in their current positions
        mySymbol.stop(currentPosition);
        var childSymbols = mySymbol.getChildSymbols();
		
		pauseAllRecursively = function(symbols){
			$.each(symbols,function(i,child){
				child.stop(child.getPosition());
				var grandChildren = child.getChildSymbols();
				if(grandChildren.length&gt;0){
					pauseAllRecursively(grandChildren);
				}
			});
		};
		pauseAllRecursively(childSymbols);
    };

    presenter.stop = function() {
        var currCompClass = presenter.Animations[currentAnimationItem - 1].compositionClass,
            myAnim = AdobeEdge.getComposition(currCompClass),
            mySymbol = myAnim.getSymbols("stage")[0];

		//stop "stage" and all its children in 0
        mySymbol.stop(0);
        var childSymbols = mySymbol.getChildSymbols();
		
		stopAllRecursively = function(symbols){
			$.each(symbols,function(i,child){
				child.stop(0);
				var grandChildren = child.getChildSymbols();
				if(grandChildren.length&gt;0){
					stopAllRecursively(grandChildren);
				}
			});
		};
		stopAllRecursively(childSymbols);
    };

    presenter.play = function(position){
		var currCompClass = presenter.Animations[currentAnimationItem - 1].compositionClass;
		var myAnim = AdobeEdge.getComposition(currCompClass);
			mySymbol = myAnim.getStage(),
			currentPosition = position != undefined ? position[0] : mySymbol.getPosition();
		
		//play "stage" and all its children from their current positions
		mySymbol.play(currentPosition);
		var childSymbols = mySymbol.getChildSymbols();
		
		playAllRecursively = function(symbols){
			$.each(symbols,function(i,child){
				child.play(child.getPosition());
				var grandChildren = child.getChildSymbols();
				if(grandChildren.length&gt;0){
					playAllRecursively(grandChildren);
				}
			});
		};
		playAllRecursively(childSymbols);
    };
	
	presenter.getEdgeComposition = function(Item){
		var i = Item != undefined ? Item[0] : currentAnimationItem;
		var currCompClass = presenter.Animations[i - 1].compositionClass;

		return AdobeEdge.getComposition(currCompClass);
    };
	
	presenter.getEdgeStage = function(Item){
		var i = Item != undefined ? Item[0] : currentAnimationItem;
		var currCompClass = presenter.Animations[i - 1].compositionClass;
		var myAnim = AdobeEdge.getComposition(currCompClass);
		
		return myAnim.getStage();
    };

    presenter.setShowErrorsMode = function(){
		return;
    };

    presenter.setWorkMode = function(){
        return;
    };

    presenter.reset = function(){
		var initAnimItem = presenter.model.initialAnimation+1;
		
		presenter.switchAnimation(initAnimItem);            
		
		presenter.stop(0);
		if(presenter.autoplayState){
			presenter.play(0);
		}

    };

    presenter.getState = function(){
		//stop "stage" and all its children
		$(Animations).each(function(i,animation){
			var currCompClass = animation.compositionClass,
				myAnim = AdobeEdge.getComposition(currCompClass),
				mySymbol = myAnim.getSymbols("stage")[0];

			mySymbol.stop(0);
			var childSymbols = mySymbol.getChildSymbols();
			
			stopAllRecursively = function(symbols){
				$.each(symbols,function(i,child){
					child.stop(0);
					var grandChildren = child.getChildSymbols();
					if(grandChildren.length&gt;0){
						stopAllRecursively(grandChildren);
					}
				});
			};
			stopAllRecursively(childSymbols);
		});
		
        return JSON.stringify({
            'currentAnimationItem' : currentAnimationItem,
            'isVisible' : presenter.isVisible
        });
    };

    presenter.setState = function(state){
        var parsedState = JSON.parse(state);
	
		currentAnimationItem = parsedState.currentAnimationItem;
		presenter.isVisible = parsedState.isVisible;
		presenter.setVisibility(presenter.isVisible);
		
        //setting definitions launched to false, so the AdobeEdge.launchComposition has to create composition again
        $.each(AdobeEdge.compositionDefns, function() {
            this.launched = false;
        });
    };
	
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="EditableWindow" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Title" name="title" nameLabel="EditableWindow_property_title" type="string"/>
        <property name="textEditor" nameLabel="EditableWindow_property_text_editor" type="html"/>
        <property name="isTextEditorContent" nameLabel="EditableWindow_property_is_text_editor_content" type="boolean"/>
        <property displayName="Header style" name="headerStyle" nameLabel="EditableWindow_property_header_style" type="string"/>
        <property displayName="File List" name="fileList" nameLabel="EditableWindow_property_file_list" type="list">
            <property displayName="ID" name="id" nameLabel="EditableWindow_property_id" type="string"/>
            <property displayName="File" name="file" nameLabel="EditableWindow_property_file" type="file"/>
        </property>
        <property displayName="Index File" name="index" nameLabel="EditableWindow_property_index_file" type="file"/>
        <property displayName="Audio File" name="audio" nameLabel="EditableWindow_property_audio_file" type="file"/>
        <property displayName="Video File" name="video" nameLabel="EditableWindow_property_video_file" type="file"/>
        <property displayName="Editing enabled" name="editingEnabled" nameLabel="EditableWindow_property_editing_enabled" type="boolean"/>
        <property displayName="Disable resize height" name="disableResizeHeight" nameLabel="EditableWindow_property_disable_resize_height" type="boolean"/>
        <property name="offlineMessage" nameLabel="EditableWindow_property_offline_message" type="text"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="EditableWindow_property_speech_texts" type="staticlist">
            <property name="openFullscreen" nameLabel="EditableWindow_property_openFullscreen" type="staticrow">
                <property name="openFullscreen" nameLabel="EditableWindow_property_openFullscreen" type="string"/>
            </property>
            <property name="closeFullscreen" nameLabel="EditableWindow_property_closeFullscreen" type="staticrow">
                <property name="closeFullscreen" nameLabel="EditableWindow_property_closeFullscreen" type="string"/>
            </property>
            <property name="closeWindow" nameLabel="EditableWindow_property_closeWindow" type="staticrow">
                <property name="closeWindow" nameLabel="EditableWindow_property_closeWindow" type="string"/>
            </property>
            <property name="textTool" nameLabel="EditableWindow_property_textTool" type="staticrow">
                <property name="textTool" nameLabel="EditableWindow_property_textTool" type="string"/>
            </property>
            <property name="highlightSelect" nameLabel="EditableWindow_property_highlightSelect" type="staticrow">
                <property name="highlightSelect" nameLabel="EditableWindow_property_highlightSelect" type="string"/>
            </property>
            <property name="pickAColor" nameLabel="EditableWindow_property_pickAColor" type="staticrow">
                <property name="pickAColor" nameLabel="EditableWindow_property_pickAColor" type="string"/>
            </property>
            <property name="yellow" nameLabel="EditableWindow_property_yellow" type="staticrow">
                <property name="yellow" nameLabel="EditableWindow_property_yellow" type="string"/>
            </property>
            <property name="blue" nameLabel="EditableWindow_property_blue" type="staticrow">
                <property name="blue" nameLabel="EditableWindow_property_blue" type="string"/>
            </property>
            <property name="red" nameLabel="EditableWindow_property_red" type="staticrow">
                <property name="red" nameLabel="EditableWindow_property_red" type="string"/>
            </property>
            <property name="green" nameLabel="EditableWindow_property_green" type="staticrow">
                <property name="green" nameLabel="EditableWindow_property_green" type="string"/>
            </property>
            <property name="white" nameLabel="EditableWindow_property_white" type="staticrow">
                <property name="white" nameLabel="EditableWindow_property_white" type="string"/>
            </property>
            <property name="noColor" nameLabel="EditableWindow_property_noColor" type="staticrow">
                <property name="noColor" nameLabel="EditableWindow_property_noColor" type="string"/>
            </property>
            <property name="reset" nameLabel="EditableWindow_property_reset" type="staticrow">
                <property name="reset" nameLabel="EditableWindow_property_reset" type="string"/>
            </property>
            <property name="image" nameLabel="EditableWindow_property_image" type="staticrow">
                <property name="image" nameLabel="EditableWindow_property_image" type="string"/>
            </property>
            <property name="audio" nameLabel="EditableWindow_property_audio" type="staticrow">
                <property name="audio" nameLabel="EditableWindow_property_audio" type="string"/>
            </property>
            <property name="video" nameLabel="EditableWindow_property_video" type="staticrow">
                <property name="video" nameLabel="EditableWindow_property_video" type="string"/>
            </property>
        </property>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="EditableWindow_property_lang_attribute" type="string"/>
    </model>
<css>div.addon-editable-window-fixed-container {
    position: fixed;
    top: 0;
    left: 0;
}

div.addon-editable-window-container.addon-editable-window-container-full-screen.ui-draggable-disabled {
    opacity: 1;
    background-image: initial;
}

div.addon-editable-window-container-full-screen {
    left: 0 !important;
}

.addon-editable-window-wrapper {
    width: 100%;
    height: 100%;
    position: absolute;
    background-color: #eaeaea;
    border: 1px solid #cccccc;
    display: flex;
    flex-flow: column;
    cursor: move;
}

.addon-editable-window-wrapper iframe {
    display: none;
}

.addon-editable-window-wrapper textarea {
    width: 100%;
    height: 100%;
}

div.addon-editable-buttons-menu {
    z-index: 203;
    position: fixed;
}

.addon-editable-close-button {
    position: absolute;
    top: 2px;
    right: 2px;
    height: 30px;
    width: 35px;
    line-height: 30px;
    text-align: center;
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    background: none;
    border: none;
    outline: none;
    cursor: pointer;
    z-index: 201;
}

button.addon-editable-full-screen-button {
    position: absolute;
    top: 8px;
    right: 40px;
    height: 20px;
    width: 20px;
    text-align: center;
    font-family: Verdana, Geneva, Tahoma, sans-serif;
    background: none;
    border: none;
    outline: none;
    cursor: pointer;
    z-index: 201;
    background-repeat: no-repeat;
}

button.addon-editable-open-full-screen-button {
    background-image: url('resources/open-fullscreen-button.png');
}

button.addon-editable-close-full-screen-button {
    background-image: url('resources/close-fullscreen-button.png');
}

.addon-editable-reset-button {
    width: 15px;
    height: 15px;
    position: absolute;
    left: 10px;
    bottom: 6px;
    border: none;
    background: url('resources/reset-button.png') no-repeat center;
    background-size: 11px 11px;
    z-index: 201;
    cursor: pointer;
}

.addon-editable-window-wrapper .header {
    min-height: 20px;
    font-size: 14px;
    padding: 8px 10px;
    width: 82%;
    overflow: hidden;
    cursor: move;
}

.addon-editable-window-wrapper audio {
    height: 35px;
    width: 100%;
    background-color: #f1f3f4;
}

.addon-editable-window-wrapper .video-wrapper {
    flex-grow: 1;
    display: grid;
    background-color: black;
    cursor: default;
}

@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
   .addon-editable-window-wrapper .video-wrapper {
        height: 100%;
   }
}

.addon-editable-window-wrapper video {
    height: 100%;
    width: 100%;
}

.addon-editable-window-wrapper .footer {
    height: 20px;
}

.addon-editable-window-wrapper .text-wrapper {
    padding: 10px;
}

.addon-editable-window-wrapper .video-wrapper .offline-video-message {
    width: 100%;
    top: 45%;
    position: absolute;
    z-index: 205;
    color: white;
    text-align: center;
    display: none;
}

.selected_module_fake {
    outline: 2px solid rgba(27,144,253,1) !important;
	-webkit-box-shadow: 0 0 2px 0 rgba(27,144,253,0.6) !important;
	box-shadow: 0 0 2px 0 rgba(27,144,253,0.6) !important;
}

.active_module_fake {
	outline: 2px solid rgba(0,255,0,1) !important;
	-webkit-box-shadow: 0 0 2px 0 rgba(0,255,0,0.6) !important;
	box-shadow: 0 0 2px 0 rgba(0,255,0,0.6) !important;
}
</css><view>&lt;div class="addon-editable-buttons-menu"&gt;
    &lt;button class="addon-editable-close-button"&gt;X&lt;/button&gt;
    &lt;button class="addon-editable-full-screen-button addon-editable-open-full-screen-button"&gt;&lt;/button&gt;
&lt;/div&gt;
&lt;div class="addon-editable-window-fixed-container"&gt;
    &lt;div class="addon-editable-window-container"&gt;
        &lt;div class="addon-editable-window-wrapper"&gt;
            &lt;div class="header"&gt;
                &lt;div class="header-icon"&gt;&lt;/div&gt;
                &lt;div class="header-text"&gt;&lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="video-wrapper"&gt;
                &lt;video controls controlsList="nodownload"&gt;&lt;/video&gt;
                &lt;div class="offline-video-message"&gt;&lt;/div&gt;
            &lt;/div&gt;
            &lt;audio controls controlsList="nodownload"&gt;&lt;/audio&gt;
            &lt;iframe class="content-iframe"&gt;&lt;/iframe&gt;
            &lt;div class="text-wrapper"&gt;
                &lt;textarea&gt;&lt;/textarea&gt;
            &lt;/div&gt;
            &lt;div class="footer"&gt;
                &lt;button class="addon-editable-reset-button"&gt;&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="addon-editable-window-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonEditableWindow_create() {

    var presenter = function () {
    };

    presenter.configuration = {
        playerController: null,
        eventBus: null,
        timeouts: [],
        view: null,
        model: null,
        isIframeLoaded: false,
        isTinyMceLoaded: false,
        isTinyMceFilled: false,
        contentLoadingLock: false,
        iframeContent: null,
        editor: null,
        textareaId: null,
        isVisible: true,
        hasHtml: false,
        hasVideo: false,
        hasAudio: false,
        heightOffset: 110,
        widthOffset: 22,
        minHeight: 300,
        maxHeight: 10000,
        minWidth: 300,
        maxWidth: 950,
        state: {
            isInitialized: false,
            content: null
        }
    };

    // these values are numbers and won't have "px" ending
    presenter.temporaryState = {
        addonFullScreenHeight: 0,
        addonHeight: 0,
        addonLeft: 0,
        addonTop: 0,
        addonWidth: 0,
        isFullScreen: false,
        scaleInfo: {
            scaleX: 1.0,
            scaleY: 1.0
        },
        scrollTop: 0
    };

    presenter.jQueryElementsCache = {
        $buttonMenu: undefined,
        $container: undefined,
        $fixedContainer: undefined,
        $fullscreenButton: undefined,
        $view: undefined
    };

    presenter.isKeyboardNavDeactivationBlocked = false;
    presenter.isEditAreaInScrollableMode = false;

    presenter.keys = {
        ARROW_UP: 38,
        ARROW_DOWN: 40
    };

    presenter.DEFAULT_TTS_PHRASES = {
        openFullscreen: "Open fullscreen",
        closeFullscreen: "Close fullscreen",
        closeWindow: "Close window",
        textTool: "Text highlighting tool",
        highlightSelect: "Highlight selected text",
        pickAColor: "Pick a color for highlighting",
        yellow: "Yellow",
        blue: "Blue",
        red: "Red",
        green: "Green",
        white: "White",
        noColor: "No color",
        reset: "Reset",
        image: "Image",
        audio: "Audio",
        video: "Video",
    };

    presenter.initJQueryCache = function($view) {
        presenter.jQueryElementsCache.$fullscreenButton = $view.find(presenter.cssClasses.fullScreenButton.getSelector());
        presenter.jQueryElementsCache.$buttonMenu = $view.find(presenter.cssClasses.buttonMenu.getSelector());
        presenter.jQueryElementsCache.$container = $view.find(presenter.cssClasses.container.getSelector());
        presenter.jQueryElementsCache.$fixedContainer = $view.find(presenter.cssClasses.fixedContainer.getSelector());
        presenter.jQueryElementsCache.$view = $view;
    };

    presenter.run = function (view, model) {
        presenter.configuration.view = view;
        // container is the div that will be draggable and resizable
        presenter.configuration.container = view.getElementsByClassName(presenter.cssClasses.container.getName())[0];
        presenter.initJQueryCache($(view));

        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration.model = presenter.validModel(upgradedModel);

        if (presenter.configuration.model.isValid) {
            presenter.configuration.container.style.width = presenter.configuration.model.width + 'px';
            presenter.configuration.container.style.height = presenter.configuration.model.height + 'px';

            presenter.configuration.textareaId = presenter.configuration.model.id + "-textarea";
            presenter.configuration.hasHtml = presenter.configuration.model.indexFile !== "";
            presenter.configuration.hasAudio = presenter.configuration.model.audioFile !== "";
            presenter.configuration.hasVideo = presenter.configuration.model.videoFile !== "";
            presenter.temporaryState.iFrameOffset = window.iframeSize.frameOffset || 0;

            presenter.init();
            presenter.hide();
        } else {
            $(view).html(presenter.configuration.model.errorMessage);
        }
    };

    presenter.init = function () {
        var $view = presenter.jQueryElementsCache.$view;
        var $container = presenter.jQueryElementsCache.$container;
        var hasHtml = presenter.configuration.hasHtml;
        var textareaId = presenter.configuration.textareaId;
        var title = presenter.configuration.model.title;
        var headerStyle = presenter.configuration.model.headerStyle;
        var $header = $container.find(".header");
        var $headerText = $container.find(".header-text");
        var disableResizeHeight = presenter.configuration.model.disableResizeHeight;

        $headerText.text(title);
        $header.addClass(headerStyle);
        if (presenter.configuration.model.isTextEditorContent) {
            presenter.handleTextContent();
            presenter.configuration.hasVideo = false;
            presenter.configuration.hasAudio = false;
            presenter.configuration.hasHtml = false;
        }

        if (presenter.configuration.hasVideo) {
            presenter.handleVideoContent();
            presenter.configuration.hasHtml = false;
            presenter.configuration.hasAudio = false;
        } else {
            $container.find(".video-wrapper").remove();
        }

        if (presenter.configuration.hasAudio) {
            presenter.handleAudioContent();
        } else {
            $container.find("audio").remove();
        }

        if (presenter.configuration.hasHtml) {
            presenter.handleHtmlContent();
        } else if (!presenter.configuration.model.isTextEditorContent) {
            $view.find(".content-iframe").remove();
            $view.find("textarea").remove();
        }

        $container.css("z-index", "1");

        if (disableResizeHeight) {
            var moduleHeight = presenter.configuration.model.height;
            presenter.configuration.minHeight = moduleHeight;
            presenter.configuration.maxHeight = moduleHeight;
        }

        // containment option disallows moving window outside of specified dom element
        $container.draggable({
            cancel: 'video, audio',
            start: function (event, ui) {
                presenter.show();
                presenter.updateScaleInfo();

                ui.helper.css('position', 'absolute'); // it removes mini jump on the beginning of dragging
                ui.position.top = ui.offset.top / presenter.temporaryState.scaleInfo.scaleX;
                ui.position.let = ui.offset.left / presenter.temporaryState.scaleInfo.scaleY;
            },
            drag: presenter.updateMenuPosition,
            stop: presenter.updateMenuPosition
        });

        $container.resizable({
            minHeight: presenter.configuration.minHeight,
            maxHeight: presenter.configuration.maxHeight,
            minWidth: presenter.configuration.minWidth,
            maxWidth: presenter.configuration.maxWidth,
            resize: function (event, ui) {
                presenter.updateButtonMenuPosition();
                if (hasHtml || presenter.configuration.model.isTextEditorContent) {
                    var heightOffset = presenter.configuration.heightOffset;
                    var widthOffset = presenter.configuration.widthOffset;
                    var newHeight = ui.size.height - heightOffset;
                    var newWidth = ui.size.width - widthOffset;
                    tinymce.get(textareaId).theme.resizeTo(newWidth, newHeight);
                }
            },
            start: function (event, ui) {
                if (hasHtml) {
                    $container.find("iframe").css("visibility", "hidden")
                }
            },
            stop: function (event, ui) {
                presenter.updateButtonMenuPosition();
                if (hasHtml) {
                    $container.find("iframe").css("visibility", "visible")
                }
            }
        });

        presenter.addHandlers($view);
        presenter.buildKeyboardController();
        presenter.setUpKeyboardNavigationStyling();
        presenter.setSpeechTexts(presenter.configuration.model.speechTexts);
    };

    presenter.updateScaleInfo = function AddonEditableWindow_getScaleInfo() {
        presenter.temporaryState.scaleInfo = presenter.configuration.playerController.getScaleInformation();
    };

    /**
     * @param event
     * @param ui
     * @param ui.helper
     * @param ui.position
     * @param ui.position.top
     * @param ui.position.left
     * @param ui.offset
     */
    presenter.updateMenuPosition = function AddonEditableWindow_updateMenuPosition(event, ui) {
        ui.position.top = ui.position.top / presenter.temporaryState.scaleInfo.scaleX;
        ui.position.left = ui.position.left / presenter.temporaryState.scaleInfo.scaleY;

        presenter.updateButtonMenuPosition();
    };

    // because draggable prevents event bubbling, button wasn't clickable on android
    // menu with buttons is now outside of draggable container and its position needs to be updated when container changes position or size
    presenter.updateButtonMenuPosition = function () {
        // selector needs to be scoped to addon id, otherwise if more than one addon were added to lesson then it wouldn't properly position $buttonMenu
        var $buttonParent = $('#' + presenter.configuration.model.id + ' ' + presenter.cssClasses.container.getSelector());
        var $buttonMenu = presenter.jQueryElementsCache.$buttonMenu;

        // icons are positioned by setting right and top css values, so div which wraps icons must be placed in top right corner of menu
        var rightWindowBorder = parseInt($buttonParent.css('left'), 10) + $buttonParent.width();
        var topWindowBorder = parseInt($buttonParent.css('top'), 10);

        $buttonMenu.css({
            top: topWindowBorder,
            left: rightWindowBorder
        });
    };

    presenter.addHandlers = function ($view) {
        $view.find(presenter.cssClasses.closeButton.getSelector()).click(presenter.closeButtonClickedCallback);
        $view.find(presenter.cssClasses.fullScreenButton.getSelector()).click(presenter.fullScreenButtonClickedCallback);
        $view.find(presenter.cssClasses.wrapper.getSelector()).click(presenter.viewClickedCallback);

        // scaling will break fixed positioning, but mobile views aren't placed in iframe so, player won't be updating scroll position
        if (MobileUtils.isMobileUserAgent(window.navigator.userAgent)) {
           window.addEventListener('scroll', presenter.handleScroll);
        }
    };

    presenter.viewClickedCallback = function () {
        presenter.show();
    };

    presenter.closeButtonClickedCallback = function () {
        if (presenter.temporaryState.isFullScreen) {
            var $view = presenter.jQueryElementsCache.$container;

            presenter.closeFullScreen($view);
        }

        presenter.hide();
    };

    presenter.fullScreenButtonClickedCallback = function () {
        var $view = presenter.jQueryElementsCache.$container;

        if (presenter.temporaryState.isFullScreen) {
            presenter.closeFullScreen($view);
        } else {
            presenter.openFullScreen($view);
        }

        presenter.updateButtonMenuPosition();
    };

    presenter.openFullScreen = function ($view) {
        presenter.updateScaleInfo();
        // so height of the window will take whole available space
        var height = (window.iframeSize.windowInnerHeight || window.innerHeight) / presenter.temporaryState.scaleInfo.scaleY;
        var width = window.innerWidth / presenter.temporaryState.scaleInfo.scaleX;

        presenter.temporaryState.isFullScreen = true;

        presenter.saveViewPropertiesToState($view);
        $view.height(height);
        $view.width(width);
        presenter.addFullScreenClasses($view);

        presenter.updateFullScreenWindowTop();
        presenter.resizeTinyMce($view.width(), height);
    };

    presenter.closeFullScreen = function ($view) {
        presenter.temporaryState.isFullScreen = false;

        presenter.setViewPropertiesFromState($view);
        presenter.removeFullScreenClasses($view);
        presenter.resizeTinyMce($view.width(), $view.height());
    };

    // save current size and position to state
    presenter.saveViewPropertiesToState = function ($view) {
        presenter.temporaryState.addonWidth = $view.width();
        presenter.temporaryState.addonHeight = $view.height();
        presenter.temporaryState.addonTop = $view.position().top;
        presenter.temporaryState.addonLeft = $view.position().left;
    };

    // restore size and position before going full screen, also add current scroll value so window is visible at once
    presenter.setViewPropertiesFromState = function ($view) {
        $view.width(presenter.temporaryState.addonWidth);
        $view.height(presenter.temporaryState.addonHeight);
        $view.css({
            top: presenter.temporaryState.addonTop + presenter.temporaryState.scrollTop,
            left: presenter.temporaryState.addonLeft
        });
    };

    presenter.addFullScreenClasses = function ($view) {
        presenter.jQueryElementsCache.$fixedContainer.addClass(presenter.cssClasses.containerFullScreen.getName());
        presenter.jQueryElementsCache.$fullscreenButton.removeClass(presenter.cssClasses.openFullScreenButton.getName());
        presenter.jQueryElementsCache.$fullscreenButton.addClass(presenter.cssClasses.closeFullScreenButton.getName());
        $view.addClass(presenter.cssClasses.containerFullScreen.getName());

        $view.resizable('disable');
        $view.draggable('disable');
    };

    presenter.removeFullScreenClasses = function ($view) {
        presenter.jQueryElementsCache.$fixedContainer.removeClass(presenter.cssClasses.containerFullScreen.getName());
        presenter.jQueryElementsCache.$fullscreenButton.removeClass(presenter.cssClasses.closeFullScreenButton.getName());
        presenter.jQueryElementsCache.$fullscreenButton.addClass(presenter.cssClasses.openFullScreenButton.getName());

        $view.removeClass(presenter.cssClasses.containerFullScreen.getName());

        $view.resizable('enable');
        $view.draggable('enable');
    };

    presenter.resizeTinyMce = function (width, height) {
        if (presenter.configuration.isTinyMceLoaded &amp;&amp; presenter.configuration.editor) {
            // tinymce can be smaller than whole window
            width -= presenter.configuration.widthOffset;
            height -= presenter.configuration.heightOffset;
            presenter.configuration.editor.theme.resizeTo(width, height);
        }
    };

    // during scroll window needs to be repositioned, so it blocks whole lesson view
    presenter.updateFullScreenWindowTop = function () {
        var $view = presenter.jQueryElementsCache.$container;
        var top = presenter.temporaryState.scrollTop;
        var properties = {
            top: top
        };

        // this is needed when embedding page has header and iFrame is not at the top of the page
        if (top &gt; presenter.temporaryState.iFrameOffset &amp;&amp; presenter.temporaryState.scaleInfo.scaleY === 1.0) {
            properties.top = (top - presenter.temporaryState.iFrameOffset) + 'px';
        }

        // on android scroll down/up can hide/show navbar which adds/subtracts available height
        if (MobileUtils.isMobileUserAgent(window.navigator.userAgent)) {
            properties.height = (window.iframeSize.windowInnerHeight || window.innerHeight) / presenter.temporaryState.scaleInfo.scaleY;
        }

        $view.css(properties);
        presenter.updateButtonMenuPosition();
    };

    presenter.handleVideoContent = function () {
        var $view = $(presenter.configuration.view);
        if (window.navigator.onLine || presenter.configuration.model.videoFile.indexOf("file:/") == 0) {
            var audioSource = presenter.configuration.model.videoFile;
            var $videoElement = $view.find("video");
            $videoElement.attr("src", audioSource);
            Object.defineProperty($videoElement[0], 'isPlaying', {
                get: function() {
                    return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
                }
            });
        } else {
            presenter.configuration.hasVideo = false;
            var $wrapper = $view.find('.offline-video-message');
            $wrapper.html(presenter.configuration.model.offlineMessage);
            $wrapper.css("display", "block");
        }
    };

    presenter.handleAudioContent = function () {
        var $view = $(presenter.configuration.view);
        var $container = presenter.jQueryElementsCache.$container;
        var audioSource = presenter.configuration.model.audioFile;
        var $audioElement = $view.find("audio");
        $audioElement.attr("src", audioSource);
        Object.defineProperty($audioElement[0], 'isPlaying', {
            get: function() {
                return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
            }
        });
        presenter.configuration.heightOffset += 35;
    };

    presenter.handleTextContent = function () {
        var height = presenter.configuration.model.height - presenter.configuration.heightOffset;
        var width = presenter.configuration.model.width - presenter.configuration.widthOffset;
        var textareaId = presenter.configuration.textareaId;
        var $view = $(presenter.configuration.view);
        $view.css("z-index", "1");

        var textarea = $view.find("textarea");
        textarea.attr("id", textareaId);

        presenter.createTinyMceAsync(textareaId, height, width);

        presenter.fillActiveTinyMce(presenter.configuration.model.textEditor, function (content) {
            setTextAreaInnerHTML(textareaId, content);

            presenter.configuration.iframeContent = content;
            $view.find(".addon-editable-reset-button").click(function () {
                    presenter.reset();
            });
        });
    };

    presenter.handleHtmlContent = function () {
        var height = presenter.configuration.model.height;
        var width = presenter.configuration.model.width;
        var indexFile = presenter.configuration.model.indexFile;
        var textareaId = presenter.configuration.textareaId;
        var $view = $(presenter.configuration.view);

        var iframe = $view.find(".content-iframe");
        var separator = (indexFile.indexOf("?") === -1) ? "?" : "&amp;";
        var source = indexFile + separator + "no_gcs=true";

        iframe.attr("onload", function () {
            presenter.configuration.isIframeLoaded = true;
        });
        iframe.attr("src", source);

        $view.css("z-index", "1");

        var textarea = $view.find("textarea");
        textarea.attr("id", textareaId);

        var widthOffset = presenter.configuration.widthOffset;
        var heightOffset = presenter.configuration.heightOffset;

        presenter.createTinyMceAsync(textareaId, height - heightOffset, width - widthOffset);

        var timeout = setTimeout(function () {
            presenter.fetchIframeContent(function (content) {
                var isInitialized = presenter.configuration.state.isInitialized;
                if (!isInitialized) {
                    presenter.configuration.contentLoadingLock = true;
                    presenter.fillActiveTinyMce(content, presenter.fillTinyMce);
                    presenter.configuration.state.isInitialized = true;
                    presenter.configuration.state.content = content;
                    presenter.configuration.contentLoadingLock = false;
                }
                presenter.removeIframe();
            });
        }, 3000);
        presenter.configuration.timeouts.push(timeout);
    };

    presenter.createTinyMceAsync = function (areaId, height, width) {
        tinymce.init({
            selector: "#" + areaId,
            plugins: "textcolor link",
            toolbar: "backcolor",
            language: "fr_FR_pure",
            textcolor_map: [
                "ffff00", "Yellow",
                "87ceeb", "Blue",
                "ffb6c1", "Red",
                "90ee90", "Green",
                "ffffff", "White"
            ],
            custom_colors: false,
            statusbar: false,
            menubar: false,
            height: height,
            width: width,
            setup: function (editor) {
                if (!presenter.configuration.model.editingEnabled) {
                    editor.on('keydown keypress keyup', function (e) {
                        e.preventDefault();
                    });
                }
            },
            readonly: !presenter.configuration.model.editingEnabled
        }).then(function (editors) {
            presenter.configuration.editor = editors[0];
            presenter.configuration.isTinyMceLoaded = true;
        });
    }

    presenter.createPreview = function (view, model) {
        presenter.configuration.view = view;
        presenter.configuration.model = presenter.validModel(model);

        if (!presenter.configuration.model.isValid) {
            $(view).html(presenter.configuration.model.errorMessage);
        }
    };

    presenter.setState = function (state) {
        var contentLoadingLock = presenter.configuration.contentLoadingLock;
        if (contentLoadingLock) {
            var timeout = setTimeout(function (state) {
                presenter.setState(state);
            }, 1000);

            presenter.configuration.timeouts.push(timeout);
        } else {
            handleState(state);
        }
    };

    function handleState(state) {
        presenter.configuration.contentLoadingLock = true;
        presenter.configuration.state = JSON.parse(state);

        var isInitialized = presenter.configuration.state.isInitialized;
        var content = presenter.configuration.state.content;

        if (isInitialized) {
            presenter.fillActiveTinyMce(content, presenter.fillTinyMce);
        }
        presenter.configuration.contentLoadingLock = false;
    };

    presenter.getState = function () {
        var editor = presenter.configuration.editor;
        var isTinyMceLoaded = presenter.configuration.isTinyMceLoaded;
        var isTinyMceFilled = presenter.configuration.isTinyMceFilled;
        if (isTinyMceLoaded &amp;&amp; isTinyMceFilled) {
            presenter.configuration.state.content = editor.getContent({format: 'raw'});
        }

        return JSON.stringify(presenter.configuration.state);
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.addDisableResizeHeight(model);
        upgradedModel = presenter.addOfflineMessage(upgradedModel);
        upgradedModel = presenter.addIsTextEditorContent(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.addDisableResizeHeight = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['disableResizeHeight']) {
            upgradedModel['disableResizeHeight'] = "False";
        }

        return upgradedModel;
    };

    presenter.addOfflineMessage = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['offlineMessage']) {
            upgradedModel['offlineMessage'] = "This video is not available offline. Please connect to the Internet to watch it.";
        }

        return upgradedModel;
    };

    presenter.addIsTextEditorContent = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['isTextEditorContent']) {
            upgradedModel['isTextEditorContent'] = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['langAttribute']) {
            upgradedModel['langAttribute'] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['speechTexts']) {
             upgradedModel['speechTexts'] = {
                openFullscreen: {openFullscreen: ""},
                closeFullscreen: {closeFullscreen: ""},
                closeWindow: {closeWindow: ""},
                textTool: {textTool: ""},
                highlightSelect: {highlightSelect: ""},
                pickAColor: {pickAColor: ""},
                yellow: {yellow: ""},
                blue: {blue: ""},
                red: {red: ""},
                green: {green: ""},
                white: {white: ""},
                noColor: {noColor: ""},
                reset: {reset: ""},
                image: {image: ""},
                audio: {audio: ""},
                video: {video: ""}
             };
        }

        return upgradedModel;
    };

    presenter.validModel = function (model) {
        var indexFile = model['index'];
        var audioFile = model['audio'];
        var videoFile = model['video'];
        var text = model['textEditor'];

        var isTextEditorContent = ModelValidationUtils.validateBoolean(model['isTextEditorContent']);
        var textEmpty = !isTextEditorContent || text === "";

        if (textEmpty &amp;&amp; indexFile === "" &amp;&amp; audioFile === "" &amp;&amp; videoFile === "") {
            return presenter.generateValidationError("Content cannot be undefined.");
        }

        var fileList = [];

        var originalFileList = model["fileList"];
        for (var i = 0; i &lt; originalFileList.length; i++) {
            var entity = originalFileList[i];
            if (entity.id != "" &amp;&amp; entity.file != "") {
                fileList.push(entity);
            }
        }

        return {
            isValid: true,
            id: model['ID'],
            fileList: fileList,
            height: model['Height'],
            width: model['Width'],
            top: model['Top'],
            left: model['Left'],
            right: model['Right'],
            bottom: model['Bottom'],
            indexFile: model['index'],
            audioFile: model['audio'],
            videoFile: model['video'],
            title: model['title'] ? model['title'] : "",
            headerStyle: model['headerStyle'] ? model['headerStyle'] : "",
            editingEnabled: ModelValidationUtils.validateBoolean(model["editingEnabled"]),
            disableResizeHeight: ModelValidationUtils.validateBoolean(model["disableResizeHeight"]),
            offlineMessage: model["offlineMessage"],
            textEditor: model['textEditor'],
            isTextEditorContent: isTextEditorContent,
            speechTexts: model['speechTexts'],
            langAttribute: model['langAttribute']
        }
    };

    presenter.setSpeechTexts = function EditableWindow_setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            openFullscreen: presenter.DEFAULT_TTS_PHRASES.openFullscreen,
            closeFullscreen: presenter.DEFAULT_TTS_PHRASES.closeFullscreen,
            closeWindow: presenter.DEFAULT_TTS_PHRASES.closeWindow,
            textTool: presenter.DEFAULT_TTS_PHRASES.textTool,
            highlightSelect: presenter.DEFAULT_TTS_PHRASES.highlightSelect,
            pickAColor: presenter.DEFAULT_TTS_PHRASES.pickAColor,
            yellow: presenter.DEFAULT_TTS_PHRASES.yellow,
            blue: presenter.DEFAULT_TTS_PHRASES.blue,
            red: presenter.DEFAULT_TTS_PHRASES.red,
            green: presenter.DEFAULT_TTS_PHRASES.green,
            white: presenter.DEFAULT_TTS_PHRASES.white,
            noColor: presenter.DEFAULT_TTS_PHRASES.noColor,
            reset: presenter.DEFAULT_TTS_PHRASES.reset,
            image: presenter.DEFAULT_TTS_PHRASES.image,
            audio: presenter.DEFAULT_TTS_PHRASES.audio,
            video: presenter.DEFAULT_TTS_PHRASES.video
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            openFullscreen: TTSUtils.getSpeechTextProperty(
                speechTexts.openFullscreen.openFullscreen,
                presenter.speechTexts.openFullscreen),
            closeFullscreen: TTSUtils.getSpeechTextProperty(
                speechTexts.closeFullscreen.closeFullscreen,
                presenter.speechTexts.closeFullscreen),
            closeWindow: TTSUtils.getSpeechTextProperty(
                speechTexts.closeWindow.closeWindow,
                presenter.speechTexts.closeWindow),
            textTool: TTSUtils.getSpeechTextProperty(
                speechTexts.textTool.textTool,
                presenter.speechTexts.textTool),
            highlightSelect: TTSUtils.getSpeechTextProperty(
                speechTexts.highlightSelect.highlightSelect,
                presenter.speechTexts.highlightSelect),
            pickAColor: TTSUtils.getSpeechTextProperty(
                speechTexts.pickAColor.pickAColor,
                presenter.speechTexts.pickAColor),
            yellow: TTSUtils.getSpeechTextProperty(
                speechTexts.yellow.yellow,
                presenter.speechTexts.yellow),
            blue: TTSUtils.getSpeechTextProperty(
                speechTexts.blue.blue,
                presenter.speechTexts.blue),
            red: TTSUtils.getSpeechTextProperty(
                speechTexts.red.red,
                presenter.speechTexts.red),
            green: TTSUtils.getSpeechTextProperty(
                speechTexts.green.green,
                presenter.speechTexts.green),
            white: TTSUtils.getSpeechTextProperty(
                speechTexts.white.white,
                presenter.speechTexts.white),
            noColor: TTSUtils.getSpeechTextProperty(
                speechTexts.noColor.noColor,
                presenter.speechTexts.noColor),
            reset: TTSUtils.getSpeechTextProperty(
                speechTexts.reset.reset,
                presenter.speechTexts.reset),
            image: TTSUtils.getSpeechTextProperty(
                speechTexts.image.image,
                presenter.speechTexts.image),
            audio: TTSUtils.getSpeechTextProperty(
                speechTexts.audio.audio,
                presenter.speechTexts.audio),
            video: TTSUtils.getSpeechTextProperty(
                speechTexts.video.video,
                presenter.speechTexts.video)
        };
    };

    presenter.generateValidationError = function (message) {
        return {
            isValid: false,
            errorMessage: message
        }
    };

    presenter.fetchIframeContent = function (callback) {
        var $view = presenter.jQueryElementsCache.$view;
        var isIframeLoaded = presenter.configuration.isIframeLoaded;

        if (isIframeLoaded) {
            var content = $view.find(".content-iframe").contents().find("body").html();
            if (content == null || content == "") {
                var timeout = setTimeout(function () {
                    presenter.fetchIframeContent(callback);
                }, 1000);
                presenter.configuration.timeouts.push(timeout);
            } else {
                presenter.configuration.iframeContent = content;
                callback(content);

                $view.find(".addon-editable-reset-button").click(function () {
                    presenter.reset();
                });
            }
        } else {
            var timeout = setTimeout(function (callback) {
                presenter.fetchIframeContent(callback)
            }, 1000);

            presenter.configuration.timeouts.push(timeout);
        }
    };

    presenter.fillActiveTinyMce = function (content, fillCallback) {
        var isTinyMceLoaded = presenter.configuration.isTinyMceLoaded;

        if (isTinyMceLoaded) {
            var timeout = setTimeout(function () {
                fillCallback.apply(null, [content]);
            }, 1000);

            presenter.configuration.timeouts.push(timeout);
        } else {
            var timeout = setTimeout(function () {
                presenter.fillActiveTinyMce(content, fillCallback);
            }, 1000);

            presenter.configuration.timeouts.push(timeout);
        }
    };

    presenter.fillTinyMce = function (content) {
        var fileList = presenter.configuration.model.fileList;
        var documentContent = new DOMParser().parseFromString(content, 'text/html');
        var textareaId = presenter.configuration.textareaId;

        for (var i = 0; i &lt; fileList.length; i++) {
            var entity = fileList[i];
            var node = documentContent.getElementById(entity.id);
            if (node != null &amp;&amp; node !== undefined) {
                node.src = entity.file;
            }
        }

        var newContent = documentContent.getElementsByTagName("body")[0].innerHTML;
        setTextAreaInnerHTML(textareaId, newContent);

        presenter.getStyles();


        presenter.linkAnchors();

        presenter.configuration.isTinyMceFilled = true;
    };

    function setTextAreaInnerHTML(textareaId, content) {
        tinymce.get(textareaId).getBody().innerHTML = presenter.textParser.parse(content);
    }

    presenter.getStyles = function() {
        var indexUrl = presenter.configuration.model.indexFile;
        $.get(indexUrl).then(
            presenter.gettingIndexSuccess,
            presenter.gettingIndexError
        );
    };

    presenter.gettingIndexSuccess = function(html) {
        var headContent = new DOMParser().parseFromString(html, 'text/html');
        var styles = [];

        presenter.configuration.model.fileList.forEach(function (entity) {
            var node = headContent.getElementById(entity.id);

            if (node !== null &amp;&amp; node !== undefined &amp;&amp; node.rel === 'stylesheet') {
                   styles.push(entity.file);
            }
        });

        presenter.addStyles(styles);
    };

    presenter.gettingIndexError = function() {
        console.error("Couldn't load index of document");
    };

    presenter.addStyles = function(styles) {
        var tinymceEditorHead = tinymce.get(presenter.configuration.textareaId).contentDocument.head;
        styles.forEach(function(styleFile) {
            var link = document.createElement("link");
            link.href = styleFile;
            link.type = 'text/css';
            link.rel = 'stylesheet';
            tinymceEditorHead.appendChild(link);
        });
    };

    presenter.linkAnchors = function () {
        var $view = $(presenter.configuration.view);
        var $anchors = $view.find("iframe").contents().find("a");
        for (var i = 0; i &lt; $anchors.length; i++) {
            var anchor = $anchors[i];
            anchor.style.cursor = "pointer";
            anchor.addEventListener("click", function () {
                var anchorElement = document.createElement("a");
                anchorElement.href = anchor.href;
                anchorElement.target = '_blank';
                var anchorEvent = document.createEvent("MouseEvents");
                anchorEvent.initEvent("click", false, true);
                anchorElement.dispatchEvent(anchorEvent);
            });
        }
    };

    presenter.removeIframe = function () {
        $(presenter.configuration.view).find(".content-iframe").remove();
    };

    presenter.centerPosition = function () {
        var $view = presenter.jQueryElementsCache.$container;
        var width = $view.width();
        var availableWidth = presenter.getAvailableWidth();

        var scrollY = presenter.temporaryState.scrollTop;
        if (scrollY == 0 &amp;&amp; presenter.configuration.playerController) {
            scrollY = presenter.configuration.playerController.iframeScroll();
        }

        var topOffset = scrollY + 25;
        var leftOffset = (availableWidth - width) / 2;

        $view.css({
            top: topOffset + 'px',
            left: leftOffset + 'px',
            right: "",
            bottom: ""
        });

        presenter.updateButtonMenuPosition();
    };

    presenter.setPositionRelative = function () {
        var $view = presenter.jQueryElementsCache.$container;
        var top = parseInt(presenter.configuration.model.top) + presenter.configuration.heightOffset;
        var left = presenter.configuration.model.left;

        $view.css({
            top: top + 'px',
            left: left + 'px',
            right: "",
            bottom: ""
        });

        presenter.updateButtonMenuPosition();
    };

    presenter.getAvailableWidth = function () {
        return $(window).width();
    };

    presenter.show = function () {
        var view = presenter.configuration.view;
        var eventBus = presenter.configuration.eventBus;
        var id = presenter.configuration.model.id;
        var $view = $(view);

        presenter.configuration.isVisible = true;

        $view.style("z-index", "3");
        $view.show();
        presenter.updateButtonMenuPosition();

        eventBus.sendEvent('ValueChanged', {
            'source': id,
            'item': '',
            'value': 'move-editable-windows',
            'score': ''
        });
    };

    presenter.openPopup = function () {
        presenter.show();
        presenter.centerPosition();
    };

    presenter.hide = function () {
        presenter.disableWCAGIfTTSOrKeyboardNav();
        presenter.configuration.isVisible = false;
        $(presenter.configuration.view).hide();
        presenter.stopAudio();
        presenter.stopVideo();

        var eventBus = presenter.configuration.eventBus;
        var id = presenter.configuration.model.id;
        eventBus.sendEvent('ValueChanged', {
            'source': id,
            'item': '',
            'value': 'close',
            'score': ''
        });
    };

    presenter.disableWCAGIfTTSOrKeyboardNav = function EditableWindow_disableWCAGIfTTSOrKeyboardNav() {
        const $element = $($(presenter.configuration.view).find(".addon_EditableWindow").context);
        if ($element.hasClass(presenter.cssClasses.selectedModule.getName()) || $element.hasClass(presenter.cssClasses.activeModule.getName())) {
            presenter.dispatchEscapeKeydownEvent();
            presenter.dispatchShiftTabKeydownEvent();
            const realElement = $(presenter.configuration.view).find(presenter.cssClasses.wrapper.getSelector());
            $(realElement[0]).removeClass(presenter.cssClasses.selectedModuleFake.getName());
            $(realElement[0]).removeClass(presenter.cssClasses.activeModuleFake.getName());
        }
    };

    presenter.dispatchEscapeKeydownEvent = function EidtableWindow_dispatchEscapeKeydownEvent () {
        const event = new KeyboardEvent('keydown', {
            code: 'Esc',
            key: 'Esc',
            charCode: 27,
            keyCode: 27,
            bubbles: true
        });
        //document.body is used instead of document, because in KeyboardNavigationController listeners are set to RootPanel, which is document.body
        document.body.dispatchEvent(event);
    };

    presenter.dispatchShiftTabKeydownEvent = function EditableWindow_dispatchShiftTabKeydownEvent () {
        const event = new KeyboardEvent('keydown', {
            code: 'Tab',
            key: 'Tab',
            charCode: 9,
            keyCode: 9,
            bubbles: true,
            shiftKey: true
        });
        //document.body is used instead of document, because in KeyboardNavigationController listeners are set to RootPanel, which is document.body
        document.body.dispatchEvent(event);
    };

    presenter.isVisible = function () {
        return presenter.configuration.isVisible;
    };

    presenter.setPlayerController = function (controller) {
        presenter.configuration.playerController = controller;
        presenter.textParser = new TextParserProxy(controller.getTextParser());

        presenter.configuration.eventBus = presenter.configuration.playerController.getEventBus();
        presenter.configuration.eventBus.addEventListener('ValueChanged', this);
        presenter.configuration.eventBus.addEventListener('ScrollEvent', this);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName === 'ValueChanged') {
            presenter.handleValueChanged(eventData);
        } else if (eventName === 'ScrollEvent') {
            presenter.handleScrollEvent(eventData);
        }
    };

    presenter.handleValueChanged = function (eventData) {
        var value = eventData.value;
        var source = eventData.source;
        var id = presenter.configuration.model.id;
        var view = presenter.configuration.view;
        var $view = $(view);

        if (value === "move-editable-windows" &amp;&amp; source !== id) {
            $view.style("z-index", "1");
        }
    };

    presenter.handleScroll = function() {
        presenter.updateScaleInfo();
        var scale = presenter.temporaryState.scaleInfo.scaleY;

        if (presenter.temporaryState.scaleInfo.scaleY !== 1) {
            presenter.updateScrollTop(window.pageYOffset / scale);
        }
    };

    presenter.handleScrollEvent = function (eventData) {
        var scrollValue = parseInt(eventData.value, 10);

        presenter.updateScrollTop(scrollValue);
    };

    presenter.updateScrollTop = function(value) {
        presenter.temporaryState.scrollTop = value ;

        if (presenter.temporaryState.isFullScreen) {
            presenter.updateFullScreenWindowTop();
        }
    };

    presenter.stopAudio = function () {
        if (presenter.configuration.hasAudio) {
            var $view = $(presenter.configuration.view);
            var audioElement = $view.find("audio")[0];
            audioElement.pause();
            audioElement.currentTime = 0;
        }
    };

    presenter.pauseOrPlayElement = function EditableWindow_pauseOrPlayElement(element) {
        if(!element.hasOwnProperty('isPlaying')) {
            console.error("EditableWindow_pauseOrPlayElement - element does not have isPlaying property!");
            return;
        }

        if(element.isPlaying) {
            element.pause();
        } else {
            element.play();
        }
    };

    presenter.pauseOrPlayAudio = function EditableWindow_pauseOrPlayAudio() {
        var $view = $(presenter.configuration.view);
        var audio = $view.find("audio")[0];
        presenter.pauseOrPlayElement(audio);
    };

    presenter.pauseOrPlayVideo = function EditableWindow_pauseOrPlayVideo() {
        var $view = $(presenter.configuration.view);
        var video = $view.find("video")[0];
        presenter.pauseOrPlayElement(video);
    };

    presenter.stopVideo = function () {
        if (presenter.configuration.hasVideo) {
            var $view = $(presenter.configuration.view);
            var audioElement = $view.find("video")[0];
            audioElement.pause();
            audioElement.currentTime = 0;
        }
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isVisible': presenter.isVisible,
            'centerPosition': presenter.centerPosition,
            'openPopup': presenter.openPopup,
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.reset = function () {
        var iframeContent = presenter.configuration.iframeContent;
        var isTinyMceLoaded = presenter.configuration.isTinyMceLoaded;
        var isContentLoadingLocked = presenter.configuration.contentLoadingLock;

        if (isTinyMceLoaded &amp;&amp; !isContentLoadingLocked) {
            presenter.configuration.contentLoadingLock = true;
            presenter.fillTinyMce(iframeContent);
            presenter.configuration.state.isInitialized = true;
            presenter.configuration.state.content = iframeContent;
            presenter.configuration.contentLoadingLock = false;
        }
    };

    presenter.onDestroy = function () {
        presenter.removeCallbacks();

        var timeouts = presenter.configuration.timeouts;
        for (var i = 0; i &lt; timeouts.length; i++) {
            clearTimeout(timeouts[i]);
        }

        try {
            presenter.configuration.editor.destroy();
        } catch (e) {
            console.log(presenter.configuration.model.id + ": cannot to destroy editor.")
        }

        try {
            tinymce.remove();
        } catch (e) {
            console.log(presenter.configuration.model.id + ": cannot to remove tinymce.")
        }

        $(presenter.configuration.view).off();
        presenter.configuration.container = null;
        presenter.configuration = null;
        presenter.jQueryElementsCache = null;
    };

    presenter.removeCallbacks = function () {
        var $view = $(presenter.configuration.view);
        $view.off('click', presenter.cssClasses.closeButton.getSelector(), presenter.closeButtonClickedCallback);
        $view.off('click', presenter.cssClasses.fullScreenButton.getSelector(), presenter.fullScreenButtonClickedCallback);
        $view.off('click', presenter.cssClasses.wrapper.getSelector(), presenter.viewClickedCallback);

        window.removeEventListener('scroll', presenter.handleScroll);
    };

    // small util class for aggregating classes and getting their selectors
    presenter.CssClass = function CssClass(name) {
        this.name = name;
    };

    presenter.CssClass.prototype.getSelector = function () {
        return "." + this.name;
    };

    presenter.CssClass.prototype.getName = function () {
        return this.name;
    };

    presenter.cssClasses = {
        fixedContainer: new presenter.CssClass("addon-editable-window-fixed-container"),
        container: new presenter.CssClass("addon-editable-window-container"),
        containerFullScreen: new presenter.CssClass("addon-editable-window-container-full-screen"),
        closeButton: new presenter.CssClass("addon-editable-close-button"),
        fullScreenButton: new presenter.CssClass("addon-editable-full-screen-button"),
        openFullScreenButton: new presenter.CssClass("addon-editable-open-full-screen-button"),
        closeFullScreenButton: new presenter.CssClass("addon-editable-close-full-screen-button"),
        wrapper: new presenter.CssClass("addon-editable-window-wrapper"),
        buttonMenu: new presenter.CssClass("addon-editable-buttons-menu"),
        selectedModule: new presenter.CssClass("ic_selected_module"),
        activeModule: new presenter.CssClass("ic_active_module"),
        selectedModuleFake: new presenter.CssClass("selected_module_fake"),
        activeModuleFake: new presenter.CssClass("active_module_fake"),
    };

    function EditableWindowKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    };

    EditableWindowKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    EditableWindowKeyboardController.prototype.constructor = EditableWindowKeyboardController;

    presenter.buildKeyboardController = function EditableWindow_buildKeyboardController () {
        presenter.keyboardControllerObject = new EditableWindowKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.setUpKeyboardNavigationStyling = function EditableWindow_setUpKeyboardNavigationStyling () {
        var element = $(presenter.configuration.view).find(".addon_EditableWindow");
        var $element = $(element.context);
        var oldStyles = $element.attr("style") || "";
        var newStyles = oldStyles + " outline: none !important; box-shadow: none !important";
        $element.attr('style', newStyles);
    };

    /**
     * Method to select or activate module.
     * This method is executed by class KeyboardNavigationController.
     *
     * @param className CSS class name to add to the view
     *
     * @return undefined
     */
    presenter.selectAsActive = function (className) {
        const view = presenter.configuration.view;
        const windowWrapper = $(view).find(presenter.cssClasses.wrapper.getSelector())[0];

        view.classList.add(className);
        if (className === presenter.cssClasses.selectedModule.getName()) {
            windowWrapper.classList.add(presenter.cssClasses.selectedModuleFake.getName());
            if (!presenter.isWCAGOn) {
                windowWrapper.focus();
            }
        }
        if (className === presenter.cssClasses.activeModule.getName()) {
            windowWrapper.classList.add(presenter.cssClasses.activeModuleFake.getName());
        }
    }

    /**
     * Method to deselect or deactivate module.
     * This method is executed by class KeyboardNavigationController.
     *
     * @param className CSS class name to remove from the view
     *
     * @return undefined
     */
    presenter.deselectAsActive = function (className) {
        const view = presenter.configuration.view;
        const windowWrapper = $(view).find(presenter.cssClasses.wrapper.getSelector())[0];

        view.classList.remove(className);
        if (className === presenter.cssClasses.selectedModule.getName()) {
            windowWrapper.classList.remove(presenter.cssClasses.selectedModuleFake.getName());
            if (!presenter.isWCAGOn) {
                windowWrapper.blur();
            }
        }
        if (className === presenter.cssClasses.activeModule.getName()) {
            windowWrapper.classList.remove(presenter.cssClasses.activeModuleFake.getName());
        }
    }

    presenter.getElementsForKeyboardNavigation = function EditableWindow_getElementsForKeyboardNavigation() {
        let fullscreenElement = $(presenter.configuration.view).find(".addon-editable-full-screen-button");
        let elements = $(presenter.configuration.view).find(".addon-editable-close-button, .mce-btn, .mce-edit-area, .addon-editable-reset-button, .video-wrapper, audio");
        return $.merge(fullscreenElement, elements);
    };

    presenter.keyboardController = function EditableWindow_keyboardController (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    EditableWindowKeyboardController.prototype.getTarget = function (element) {
        return $(element);
    };

    EditableWindowKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        this.readCurrentElement();
    };

    EditableWindowKeyboardController.prototype.switchToFirstVisibleElement = function () {
        for(let i=0; i&lt;this.keyboardNavigationElementsLen; i++) {
            const element = this.keyboardNavigationElements[i];
            if(this.isElementHidden(element)) {
                this.lastVisibleElementIndex = i;
                this.keyboardNavigationCurrentElementIndex = i;
                this.keyboardNavigationCurrentElement = element;
                this.mark(element);
                return;
            }
        }
    };

    EditableWindowKeyboardController.prototype.nextElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(1);

        if(this.isElementHidden(this.getCurrentElement())) {
            this.nextElement(event);
        }
    };

    EditableWindowKeyboardController.prototype.previousElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(-1);

        if(this.isElementHidden(this.getCurrentElement())) {
            this.previousElement(event);
        }
    };

    EditableWindowKeyboardController.prototype.isElementHidden = function (element) {
        const elementHeight = element[0].offsetHeight;
        const elementWidth = element[0].offsetWidth;
        const isDisplayed = $(element).css('display') !== 'none';
        const isVisible = $(element).css('visibility') === 'visible';

        return elementHeight === 0 || elementWidth === 0 || !isDisplayed || !isVisible;
    };

    EditableWindowKeyboardController.prototype.getCurrentElement = function () {
		return this.getTarget(this.keyboardNavigationCurrentElement, false);
	};

    EditableWindowKeyboardController.prototype.enter = function EditableWindow_enter (event) {
        KeyboardController.prototype.enter.call(this, event);
        if(this.keyboardNavigationCurrentElementIndex === 0) {
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        }
        this.readCurrentElement();
    };

    EditableWindowKeyboardController.prototype.setElements = function EditableWindow_setElements (elements) {
        KeyboardController.prototype.setElements.call(this, elements);

        if (this.keyboardNavigationActive) {
            this.switchToFirstVisibleElement();
        }
    }

    EditableWindowKeyboardController.prototype.select = function EditableWindow_select (event) {
        const element = this.getTarget(this.keyboardNavigationCurrentElement);
        if ($(element).hasClass("mce-btn") &amp;&amp; presenter.configuration.model.editingEnabled) {
            presenter.isKeyboardNavDeactivationBlocked = true;
            KeyboardController.prototype.setElements.call(this, presenter.getMceBtnElements());
            document.activeElement.blur();
            this.readCurrentElement();
        } else if(presenter.isColorHighlightElement()) {
            element[0].click();
            document.activeElement.blur();
        } else if(presenter.isColorPickElement()) {
            presenter.closeAllColorPickPanels();
            element[0].click();
            KeyboardController.prototype.setElements.call(this, presenter.getColorPaletteElements());
            this.readCurrentElement();
        } else if(presenter.isInsideColorPick()) {
            element[0].childNodes[0].click();
            document.activeElement.blur();
            KeyboardController.prototype.setElements.call(this, presenter.getMceBtnElements());
            KeyboardController.prototype.markCurrentElement.call(this, 1);
            this.readCurrentElement();
        } else if(element.hasClass("mce-edit-area") &amp;&amp; presenter.configuration.model.editingEnabled) {
            presenter.configuration.editor.execCommand('mceCodeEditor');
        } else if(element.hasClass("mce-edit-area") &amp;&amp; !presenter.configuration.model.editingEnabled) {
            presenter.handleEditAreaScrolling(element);
        } else if(element.hasClass("addon-editable-close-button")) {
            element.click();
        } else if(element[0].nodeName === "AUDIO") {
            presenter.pauseOrPlayAudio();
        } else if(element.hasClass("video-wrapper")) {
            presenter.pauseOrPlayVideo();
        } else {
            element.click();
        }
    };

    EditableWindowKeyboardController.prototype.escape = function (event) {
        if (presenter.isInsideColorPick()) {
            KeyboardController.prototype.setElements.call(this, presenter.getMceBtnElements());
            KeyboardController.prototype.markCurrentElement.call(this, 1);
            this.getTarget(this.keyboardNavigationCurrentElement)[0].click();
            this.readCurrentElement();
        } else if (presenter.isInsideMceBtn()) {
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
            KeyboardController.prototype.markCurrentElement.call(this, 2);
            this.readCurrentElement();
        } else if (presenter.isEditAreaInScrollableMode) {
            presenter.escapeEditAreaScrollableMode();
        } else {
            presenter.isKeyboardNavDeactivationBlocked = false;
            KeyboardController.prototype.escape.call(this, event);
        }
    };

    presenter.getMceBtnElements = function EditableWindow_getMceBtnElements() {
        return $(presenter.configuration.view).find(".mce-btn")[0].childNodes;
    };

    presenter.getColorPaletteElements = function EditableWindow_getColorPaletteElements() {
        return $(".mce-floatpanel:visible").find(".mce-grid-cell");
    };

    presenter.closeAllColorPickPanels = function EditableWindow_closeAllColorPickPanels() {
        $(".mce-colorbutton.mce-active").each(function () {
            const element = $(this);
            element[0].childNodes[1].click();
        });
    };

    presenter.isInsideMceBtn = function EditableWindow_isInsideMceBtn() {
        return presenter.keyboardControllerObject.keyboardNavigationElements.length === 2;
    };

    presenter.isColorHighlightElement = function EditableWindow_isColorHighlightElement() {
        return presenter.isInsideMceBtn() &amp;&amp;
            presenter.keyboardControllerObject.keyboardNavigationCurrentElement === presenter.keyboardControllerObject.keyboardNavigationElements[0];
    };

    presenter.isColorPickElement = function EditableWindow_isColorPickElement() {
          return presenter.isInsideMceBtn() &amp;&amp;
            presenter.keyboardControllerObject.keyboardNavigationCurrentElement === presenter.keyboardControllerObject.keyboardNavigationElements[1];
    };

    presenter.isInsideColorPick = function EditableWindow_isInsideColorPick() {
        return presenter.keyboardControllerObject.keyboardNavigationElements.length === 6 &amp;&amp;
            $(presenter.keyboardControllerObject.getTarget(presenter.keyboardControllerObject.keyboardNavigationCurrentElement)).hasClass("mce-grid-cell");
    };

    presenter.isDeactivationBlocked = function EditableWindow_isDeactivationBlocked() {
        return presenter.isKeyboardNavDeactivationBlocked;
    };

    presenter.handleEditAreaScrolling = function EditableWindow_handleEditAreaScrolling (element) {
        const editorIframe = element[0].childNodes[0];
        const content = (editorIframe.contentDocument || editorIframe.contentWindow.document).documentElement;
        if (!isContentScrollable(content)) {
            return;
        };

        presenter.isKeyboardNavDeactivationBlocked = true;
        presenter.isEditAreaInScrollableMode = true;
        presenter.keyboardControllerObject.setElements($(presenter.configuration.view).find(".mce-edit-area"));
        presenter.overrideKeyUpAndDownHandlers(content);
    };

    function isContentScrollable(content) {
        return content.scrollHeight &gt; content.clientHeight;
    };

    presenter.overrideKeyUpAndDownHandlers = function EditableWindow_overrideKeyUpAndDownHandlers(content) {
        this.keyboardControllerObject.mapping[presenter.keys.ARROW_UP] = function () {content.scrollTop -= 10; };
        this.keyboardControllerObject.mapping[presenter.keys.ARROW_DOWN] = function () {content.scrollTop += 10; };
    };

    presenter.restoreDefualtKeyUpAndDownHandlers = function EditableWindow_restoreDefualtKeyUpAndDownHandlers () {
        this.keyboardControllerObject.mapping[presenter.keys.ARROW_UP] = this.keyboardControllerObject.previousRow;
        this.keyboardControllerObject.mapping[presenter.keys.ARROW_DOWN] = this.keyboardControllerObject.nextRow;
    };

    presenter.escapeEditAreaScrollableMode = function EditableWindow_escapeEditAreaScrollableMode () {
        this.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
        this.keyboardControllerObject.markCurrentElement(3);
        presenter.isEditAreaInScrollableMode = false;
        presenter.restoreDefualtKeyUpAndDownHandlers();
    };
    
    EditableWindowKeyboardController.prototype.readCurrentElement = function () {
        let text = "";
        const element = this.getTarget(this.keyboardNavigationCurrentElement);

        if (element.hasClass("addon-editable-open-full-screen-button")) {
            text = presenter.speechTexts.openFullscreen;
        } else if(element.hasClass("addon-editable-close-full-screen-button")) {
            text = presenter.speechTexts.closeFullscreen;
        } else if(element.hasClass("addon-editable-close-button")) {
            text = presenter.speechTexts.closeWindow;
        } else if(element.hasClass("mce-btn")) {
            text = presenter.speechTexts.textTool;
        } else if(presenter.isColorHighlightElement()) {
            text = presenter.speechTexts.highlightSelect;
        } else if(presenter.isColorPickElement()) {
            text = presenter.speechTexts.pickAColor;
        } else if(presenter.isInsideColorPick()) {
            const key = presenter.getTTSKeyBasedOnColor(element);
            text = presenter.speechTexts[key];
        } else if(element.hasClass("mce-edit-area")) {
            text = presenter.getContentToRead();
        } else if(element.hasClass("addon-editable-reset-button")) {
            text = presenter.speechTexts.reset;
        } else if(element[0].nodeName === "AUDIO") {
            text = presenter.speechTexts.audio;
        } else if(element.hasClass("video-wrapper")) {
            text = presenter.speechTexts.video;
        }

        presenter.speak(text);
    };

    //images are temporarily replaced with it's alt text wrapped in paragraph in purpose to getContent with text- this allows to avoid manual parsing HTML
    //after all, originalContent is being restored to editor
    presenter.getContentToRead = function EditableWindow_getContentToRead () {
        const rawHTMLContent = presenter.configuration.editor.getContent({format : 'raw'});
        const regex = /&lt;img .*?alt="(.*?)".*?&gt;/gm;
        const contentWithoutImages = rawHTMLContent.replace(regex, `&lt;p&gt;${presenter.speechTexts.image} $1&lt;/p&gt;`);
        const sanitizedContent = window.xssUtils.sanitize(contentWithoutImages);
        return window.TTSUtils.getTextVoiceArrayFromElement($(sanitizedContent), presenter.configuration.model.langAttribute);
    };

    presenter.getTTSKeyBasedOnColor = function EditableWindow_getTTSKeyBasedOnColor (element) {
        if (this.keyboardControllerObject.keyboardNavigationElementsLen -1 === this.keyboardControllerObject.keyboardNavigationCurrentElementIndex) {
            return "noColor";
        }
        return $(element[0].childNodes[0]).attr("title").toLowerCase();
    };

    presenter.speak = function EditableWindow_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.configuration.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function EditableWindow_setWCAGStatus(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function EditableWindow_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}

AddonEditableWindow_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon id="Event_Listener">
	<model>
	</model>
<css>.event-listener-body,
.event-listener-header,
.event-listener-clear {
    text-align: center;
    font-weight: bold;
    border: 2px dashed #999999;
    padding: 3px;
    color: red;
    width: 100%;
}

.event-listener-header {
    height: 5%;
}

.event-listener-body {
    height: 85%;
    overflow: auto;
}

.event-info {
    color: black;
    border-bottom: 1px dotted black;
    font-weight: normal;
}

.event-name {
    color: red;
    font-weight: bold;
}

.event-listener-clear {
    display: none;
    color: #00008b;
    border-top-width: 0;
    border-bottom-width: 0;
    cursor: pointer;
}
</css><view>&lt;div class="event-listener-header"&gt;No events intercepted&lt;/div&gt;
&lt;div class="event-listener-clear"&gt;Clear events log&lt;/div&gt;
&lt;div class="event-listener-body"&gt;&lt;/div&gt;
</view><preview>&lt;div class="event-listener-header"&gt;No events intercepted&lt;/div&gt;
&lt;div class="event-listener-clear"&gt;Clear events log&lt;/div&gt;
&lt;div class="event-listener-body"&gt;&lt;/div&gt;
</preview><presenter>function AddonEvent_Listener_create() {
    var presenter = function() {};

    var playerController;
    var eventBus;
    var $view;
    var eventsCount = 0;

    presenter.STANDARD_EVENTS = {
        'ValueChanged': 'vc',
        'Definition': 'de',
        'ItemSelected': 'is',
        'ItemConsumed': 'ic',
        'ItemReturned': 'ir',
        'PageLoaded': 'pl',
        'PageAllOK': 'pa',
        'ShowAnswers': 'sa',
        'HideAnswers': 'ha',
        'Done': 'do',
        'AllAttempted': 'aa',
        'NotAllAttempted': 'naa',
        'LimitedCheck': 'lc'
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    function insertEventInfo(eventData, infoElement) {
        for (var eventDataName in eventData) {
            if (!eventData.hasOwnProperty(eventDataName)) continue;

            var element = document.createElement('div');
            $(element).text(eventDataName + ': "' + eventData[eventDataName] + '"');
            $(infoElement).append(element);
        }
    }

    function updateHeader() {
        $view.find('.event-listener-header').text('Intercepted ' + eventsCount + ' events');
    }

    function createEventInfoElement(eventName) {
        var eventDateTime = new Date(),
            eventFormattedDate = eventDateTime.getHours() + ":" +
            eventDateTime.getMinutes() + ":" + eventDateTime.getSeconds(),
            eventInfo = document.createElement('div'),
            eventNameElement = document.createElement('div'),
            eventDate = document.createElement('div');

        $(eventInfo).addClass('event-info');
        $(eventNameElement).addClass('event-name');
        $(eventNameElement).text("Received event of type " + eventName);
        $(eventInfo).append(eventNameElement);

        $(eventDate).text("Date: " + eventFormattedDate);
        $(eventInfo).append(eventDate);

        return eventInfo;
    }

    presenter.onEventReceived = function(eventName, eventData) {
        var eventInfo = createEventInfoElement(eventName);
        insertEventInfo(eventData, eventInfo);

        if (!eventsCount) {
            $view.find('.event-listener-body').html(eventInfo);
            $view.find('.event-listener-clear').show();
        } else {
            $view.find('.event-listener-body .event-info:first').before(eventInfo);
        }

        eventsCount++;
        updateHeader();
    };

    presenter.run = function(view){
        eventBus = playerController.getEventBus();

        $.each(presenter.STANDARD_EVENTS, function(name, _) {
            eventBus.addEventListener(name, presenter);
        });

        $view = $(view);

        $view.find('.event-listener-clear').click(function () {
            eventsCount = 0;
            $view.find('.event-listener-header').text('No events intercepted');
            $view.find('.event-info').remove();
            $(this).hide();
        });
    };

    return presenter;
}
</presenter></addon><addon id="Event_Sender">
	<model>
	</model>
<css>.event-sender-table {
    width: 100%;
    height: 100%;
    text-align: center;
}

.event-sender-table-left {
    width: 40%;
    font-weight: bold;
    border-right: 1px dotted black;
}

.event-sender-table th {
    font-weight: bold;
    border-bottom: 1px dotted black;
}

.event-sender-table button {
    width: 80%;
    border: 1px dotted black;
}

.event-sender-table select {
    width: 95%;
}
</css><view>&lt;table class="event-sender-table"&gt;
    &lt;tr&gt;
        &lt;th class="event-sender-table-left"&gt;Field&lt;/th&gt;
        &lt;th&gt;Value&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Type:&lt;/td&gt;
        &lt;td&gt;
            &lt;select id="type"&gt;
                &lt;option value="ValueChanged" selected&gt;ValueChanged&lt;/option&gt;
                &lt;option value="ItemConsumed"&gt;ItemConsumed&lt;/option&gt;
                &lt;option value="ItemReturned"&gt;ItemReturned&lt;/option&gt;
                &lt;option value="ItemSelected"&gt;ItemSelected&lt;/option&gt;
                &lt;option value="Definition"&gt;Definition&lt;/option&gt;
            &lt;/select&gt;
        &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Source:&lt;/td&gt;
        &lt;td&gt;&lt;input id="source"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Item:&lt;/td&gt;
        &lt;td&gt;&lt;input id="item"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Value&lt;/td&gt;
        &lt;td&gt;&lt;input id="value"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Score&lt;/td&gt;
        &lt;td&gt;&lt;input id="score"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr class="send-button"&gt;
        &lt;td colspan="2"&gt;&lt;button&gt;Send event&lt;/button&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;
</view><preview>&lt;table class="event-sender-table"&gt;
    &lt;tr&gt;
        &lt;th class="event-sender-table-left"&gt;Field&lt;/th&gt;
        &lt;th&gt;Value&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Type:&lt;/td&gt;
        &lt;td&gt;
            &lt;select id="type"&gt;
                &lt;option value="ValueChanged" selected&gt;ValueChanged&lt;/option&gt;
                &lt;option value="ItemConsumed"&gt;ItemConsumed&lt;/option&gt;
                &lt;option value="ItemReturned"&gt;ItemReturned&lt;/option&gt;
                &lt;option value="ItemSelected"&gt;ItemSelected&lt;/option&gt;
                &lt;option value="Definition"&gt;Definition&lt;/option&gt;
            &lt;/select&gt;
        &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Source:&lt;/td&gt;
        &lt;td&gt;&lt;input id="source"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Item:&lt;/td&gt;
        &lt;td&gt;&lt;input id="item"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Value&lt;/td&gt;
        &lt;td&gt;&lt;input id="value"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td class="event-sender-table-left"&gt;Score&lt;/td&gt;
        &lt;td&gt;&lt;input id="score"&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr class="send-button"&gt;
        &lt;td colspan="2"&gt;&lt;button&gt;Send event&lt;/button&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;
</preview><presenter>function AddonEvent_Sender_create() {
    var presenter = function () {};
    presenter.playerController = null;

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.run = function(view, model) {
        $(view).find('.event-sender-table button').click(function() {
            var eventData = {
                    source: $(view).find('.event-sender-table #source').val(),
                    item: $(view).find('.event-sender-table #item').val(),
                    value: $(view).find('.event-sender-table #value').val(),
                    score: $(view).find('.event-sender-table #score').val()
                },
                type = $(view).find('.event-sender-table #type').val();

            presenter.eventBus.sendEvent(type, eventData);
        });

        $(view).find("input, select, button").click(function(e) {
            e.stopPropagation();
        });
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="External_Link_Button" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="Title" nameLabel="External_Link_Button_property_title" type="string"/>
		<property name="Image" nameLabel="External_Link_Button_property_image" type="image"/>
		<property displayName="URL" name="URI" nameLabel="External_Link_Button_property_uri" type="string"/>
        <property displayName="Target type" name="targetType" nameLabel="External_Link_Button_property_target_type" type="{Blank, Top}"/>
	</model>
<css>.external-link-button-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.external-link-button-element {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    background: transparent;
    background: url('resources/elb-button.png') no-repeat center;
    cursor: pointer;
}

.external-link-button-title,
.external-link-button-image {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    cursor: pointer;
    position: absolute;
}

.external-link-button-title  {
	color: #00F;
	text-decoration: none;
}
</css><view>&lt;div class="external-link-button-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="external-link-button-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonExternal_Link_Button_create() {
    var presenter = function() {};
    
    presenter.ERROR_CODES = {
    	'M01': 'URL property cannot be empty!'
    };
    
    presenter.DISPLAY_CONTENT_TYPE = {
        NONE: 0,
        TITLE: 1,
        IMAGE: 2,
        BOTH: 3
    };

    presenter.playerController = undefined;
    presenter.isWCAGOn = false;

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.setElementsDimensions = function (model, wrapper, element) {
        var viewDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var viewDistances = DOMOperationsUtils.calculateOuterDistances(viewDimensions);

        presenter.$view.css({
            width:(model.Width - viewDistances.horizontal) + 'px',
            height:(model.Height - viewDistances.vertical) + 'px'
        });

        DOMOperationsUtils.setReducedSize(presenter.$view, wrapper);
        DOMOperationsUtils.setReducedSize(wrapper, element);
    };

    presenter.createImageElement = function (element) {
        var $imageElement = $(document.createElement('img'));

        $imageElement.addClass('external-link-button-image');
        $imageElement.attr('src', presenter.configuration.image);

        $(element).append($imageElement);
    };
    
    presenter.createTitleElement = function (element) {
        var $titleElement = $(document.createElement('span'));

        $titleElement.addClass('external-link-button-title');
        $titleElement.html(presenter.configuration.title);

        $(element).append($titleElement);
    };

    presenter.createElements = function (wrapper) {
        var $ahref = $(document.createElement('a'));
        presenter.$ahref = $ahref;
        $ahref.attr('href',presenter.configuration.URI);

        switch(true) {
            case (presenter.configuration.targetType == 'Blank'):
            case window.navigator.userAgent.toLowerCase().includes('android'):
                $ahref.attr('target','_blank');
                break;
            case (presenter.configuration.targetType == 'Top'):
                $ahref.attr('target','_top');
                break;
            default:
                $ahref.attr('target','_blank');
                break;
        }

        $ahref.click(function (event) { event.stopPropagation(); });

        var $element = $(document.createElement('div'));
        $element.addClass('external-link-button-element');
        
        switch (presenter.configuration.displayContent) {
	        case presenter.DISPLAY_CONTENT_TYPE.TITLE:
	            presenter.createTitleElement($element);
	            break;
	        case presenter.DISPLAY_CONTENT_TYPE.IMAGE:
	            presenter.createImageElement($element);
	            break;
	        case presenter.DISPLAY_CONTENT_TYPE.BOTH:
	            presenter.createImageElement($element);
	            presenter.createTitleElement($element);
	            break;
        }
        
        $ahref.append($element);
        wrapper.append($ahref);

        return $element;
    };
    
    presenter.getWrapper = function () {
    	return presenter.$view.find('.external-link-button-wrapper');
    };

    presenter.isLocalResource = function (uri) {
        var regex = new RegExp('^\\.\\.\/resources\/[0-9]*\\.[a-zA-Z0-9]+$');

        return regex.test(uri);
    };

    presenter.fixLocalResourceURI = function () {
        var currentPageIndex = presenter.playerController.getCurrentPageIndex(),
            currentPage = presenter.playerController.getPresentation().getPage(currentPageIndex),
            pageBaseURL = currentPage.getBaseURL();

        presenter.configuration.URI = pageBaseURL + presenter.configuration.URI;
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.$view = $(view);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
        	DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
        	return;
        }

        if (!isPreview &amp;&amp; presenter.isLocalResource(presenter.configuration.URI)) {
            presenter.fixLocalResourceURI();
        }

        var $wrapper = presenter.getWrapper();
        var $element = presenter.createElements($wrapper);
        
        presenter.setElementsDimensions(model, $wrapper, $element);
        presenter.buildKeyboardController();
    };

    presenter.createPreview = function(view, model) {
    	presenter.presenterLogic(view, model, true);

        presenter.setVisibility(true);
    };

    presenter.run = function(view, model){
    	presenter.presenterLogic(view, model, false);

        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
    };

    presenter.validateString = function (imageSrc) {
        var isEmpty = ModelValidationUtils.isStringEmpty(imageSrc);

        return {
            isEmpty: isEmpty,
            value: isEmpty ? "" : imageSrc
        };
    };

    presenter.determineDisplayContent = function(title, image) {
        var displayContent = presenter.DISPLAY_CONTENT_TYPE.NONE;

        if (!title.isEmpty &amp;&amp; image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.TITLE;
        } else if (title.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.IMAGE;
        } else if (!title.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.BOTH;
        }

        return displayContent;
    };
    
    presenter.validateModel = function (model) {
    	if (ModelValidationUtils.isStringEmpty(model.URI)) {
    		return { isValid: false, errorCode: 'M01' };
    	}
    	
    	var image = presenter.validateString(model.Image);
    	var title = presenter.validateString(model.Title);
    	
        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        
        return {
        	displayContent: presenter.determineDisplayContent(title, image),
        	isValid: true,
            title: model.Title,
            image: image.value,
            URI: model.URI.trim(),
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            targetType: model['targetType']
        };
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };    
    
    presenter.show = function() {
        presenter.configuration.isVisible = true;
        presenter.setVisibilityFromConfig();
    };

    presenter.hide = function() {
        presenter.configuration.isVisible = false;
        presenter.setVisibilityFromConfig();
    };
    
    presenter.setVisibilityFromConfig = function() {
    	presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.reset = function() {
    	presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
    	presenter.setVisibilityFromConfig();
    };
    
    presenter.getState = function() {
    	return JSON.stringify({
    		isVisible: presenter.configuration.isVisible
    	});
    };
    
    presenter.setState = function(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        var state = JSON.parse(stateString);

        presenter.configuration.isVisible = state.isVisible;
        presenter.setVisibilityFromConfig();
    };

    function ExternalLinkKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    ExternalLinkKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    ExternalLinkKeyboardController.prototype.constructor = ExternalLinkKeyboardController;

    presenter.buildKeyboardController = function AddonExternal_link_buildKeyboardController () {
        presenter.keyboardControllerObject = new ExternalLinkKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function AddonExternal_link_getElementsForKeyboardNavigation () {
        return presenter.$view.find(".external-link-button-wrapper")
    };


    presenter.keyboardController = function AddonExternal_link_keyboardController (keycode, isShiftKeyDown, event) {
        if (keycode == window.KeyboardControllerKeys.SPACE ||
            keycode == window.KeyboardControllerKeys.ARROW_UP ||
            keycode == window.KeyboardControllerKeys.ARROW_DOWN ||
            keycode == window.KeyboardControllerKeys.ESC)
        {
            event.preventDefault();
        }

        if (keycode === window.KeyboardControllerKeys.ENTER) {
            presenter.speak(presenter.configuration.title.trim());
        }

        if (keycode === window.KeyboardControllerKeys.SPACE) {
            presenter.$ahref[0].click();
        }
    };

    ExternalLinkKeyboardController.prototype.getTarget = function (element) {
        return $(element);
    };

    presenter.getTextToSpeechOrNull = function AddonExternal_link_getTextToSpeechOrNull (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function AddonExternal_link_setWCAGStatus (isOn) {
        presenter.isWCAGOn = isOn;
    };

    presenter.speak = function AddonExternal_link_speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="FigureDrawing" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="3DGrid" nameLabel="FigureDrawing_property_3d_grid" type="boolean"/>
        <property name="ShowGrid" nameLabel="FigureDrawing_property_show_grid" type="boolean"/>
        <property name="Grid" nameLabel="FigureDrawing_property_grid_step" type="string"/>
        <property name="Radius" nameLabel="FigureDrawing_property_point_radius" type="string"/>
        <property name="StartingLines" nameLabel="FigureDrawing_property_starting_lines" type="text"/>
        <property name="Answer" nameLabel="FigureDrawing_property_answer" type="text"/>
        <property name="IsActivity" nameLabel="FigureDrawing_property_is_activity" type="boolean"/>
        <property name="IsDisabled" nameLabel="FigureDrawing_property_is_disabled" type="boolean"/>
        <property name="Coloring" nameLabel="FigureDrawing_property_coloring" type="boolean"/>
        <property name="DefColor" nameLabel="FigureDrawing_property_default_color" type="string"/>
        <property name="StartingColors" nameLabel="FigureDrawing_property_starting_colors" type="text"/>
        <property name="AnswerColors" nameLabel="FigureDrawing_property_answer_colors" type="list">
            <property name="Figure" nameLabel="FigureDrawing_property_figure" type="string"/>
            <property name="Color" nameLabel="FigureDrawing_property_point_and_color" type="string"/>
        </property>
        <property name="BlockColoring" nameLabel="FigureDrawing_property_block_coloring" type="boolean"/>
        <property name="ShowAns" nameLabel="FigureDrawing_property_show_answers_in_editor" type="boolean"/>
    </model>
<css>.figure {
    -ms-touch-action: none;
    touch-action: none;
}
.figure.disabled{
}
.canvas {
    position:absolute;
}
.chart{
    position:absolute;
}
.chart.disabled{
    background: rgba(0,0,0,0.1);
}
.point {
    fill: black;
    stroke-width: 20px;
    stroke: #A52A2A;
    stroke-opacity: 0;
    cursor:pointer;
}

.point.selected {
    fill: #0099CC;
}

.grid {
    stroke:#7A8B8B;
    stroke-width:1;
    stroke-dasharray: 5, 5;
    stroke-linecap: round;
}
.line {
    stroke:#05B8CC;
    stroke-width:4;
    stroke-linecap: round;
}
.line.nonremovable {
    stroke: black;
}
.line.correct{
    stroke: green;
}
.line.wrong{
    stroke: red;
}
.line.show-answers{
    stroke: gray;
}
.templine {
    stroke:#4C92FD;
    stroke-width:4;
    stroke-linecap: round;
}
.figure .icon-container {
    border: 1px solid #111111;
    border-radius: 50%;
    font-size: 10px;
    height: 6px;
    line-height: 6px;
    padding: 2px;
    position: absolute;
    width: 6px;
    font-weight: bold;
}
.coordinates {
    width: 35px;
    height: 22px;
    border: 1px solid #696969;
    border-radius: 3px;
    position: absolute;
    top: -34px;
    left: 0px;
    font-size: 9px;
    padding: 5px;
    line-height: 11px;
}
.figure .icon-container.wrong {
    background-color: #ff5599;
}
.figure .icon-container.correct {
    background-color: #55ff99;
}
.figure .icon-container.correct:after {
    content: 'v';
}
.figure .icon-container.wrong:after {
    content: 'x';
}
.figure.deflines:before {
    content: 'Starting lines are incorrect!';
}
.figure.answerlines:before {
    content: 'Answer lines are incorrect!';
}
.figure.radius:before {
    content: 'Radius is incorrect!';
}
.figure.nonremanswer:before {
    content: 'At least one of the answers is a nonremovable starting line!';
}
.figure.colorerror:before {
    content: 'Error in the color definition!';
}
.figure.startingcolor:before {
    content: 'Error in the starting color definition!';
}
.figure.answerfigure:before {
    content: 'Error in the answer figure definition!';
}
.figure.grid:before {
    content: 'Enter a correct grid step!';
}
.figure.defcolorerror:before {
    content: 'Enter a correct default color!';
}
</css><view>&lt;div class="figure"&gt;&lt;/div&gt;
</view><preview>&lt;div class="figure"&gt;&lt;/div&gt;
</preview><presenter>function AddonFigureDrawing_create(){
    var presenter = function(){}
    presenter.error = false;
    presenter.isEraser = false;
    presenter.isStarted = false;
    presenter.isErrorMode = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.GSAcounter = 0;
    presenter.savedLinesIdsSA = [];

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'isAllOK'.toLowerCase():
                presenter.isAllOK();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'setColor'.toLowerCase():
                presenter.setColor(params[0]);
                break;
            case 'setDrawMode'.toLowerCase():
                presenter.setDrawMode();
                break;
            case 'setColorMode'.toLowerCase():
                presenter.setColorMode();
                break;
            case 'setEraserOn'.toLowerCase():
                presenter.setEraserOn();
                break;
            case 'allLinesDrawn'.toLowerCase():
                presenter.allLinesDrawn();
                break;
            case 'isDrawn'.toLowerCase():
                presenter.isDrawn(params[0],params[1],params[2],params[3]);
                break;
            case 'countDrawnLines'.toLowerCase():
                presenter.countDrawnLines();
                break;
            case 'markAsCorrect'.toLowerCase():
                presenter.markAsCorrect();
                break;
            case 'markAsWrong'.toLowerCase():
                presenter.markAsWrong();
                break;
            case 'markAsNeutral'.toLowerCase():
                presenter.markAsNeutral();
                break;
        }
    };

    presenter.markAsCorrect = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.$view.find('.figure').removeClass('wrong');
        presenter.$view.find('.figure').addClass('correct');
    };

    presenter.markAsWrong = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.$view.find('.figure').removeClass('correct');
        presenter.$view.find('.figure').addClass('wrong');
    };

    presenter.markAsNeutral = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.$view.find('.figure').removeClass('correct');
        presenter.$view.find('.figure').removeClass('wrong');
    };

    presenter.isDrawn = function(column1, row1, column2, row2) {
        var firstRow, firstColumn, secondRow, secondColumn, col1, col2, ro1, ro2, i;
        if (column1 &lt; column2 || (column1 == column2 &amp;&amp; row1 &lt; row2)) {
            firstColumn = column1;
            firstRow = row1;
            secondColumn = column2;
            secondRow = row2;
        } else {
            firstColumn = column2;
            firstRow = row2;
            secondColumn = column1;
            secondRow = row1;
        }
        var steps = Math.abs(gcd(secondRow-firstRow, secondColumn-firstColumn));
        if (presenter.grid3D &amp;&amp; (secondRow-firstRow+secondColumn-firstColumn)/steps %2 != 0) steps = 0.5 * steps;
        var stepX = parseInt((secondColumn-firstColumn)/steps);
        var	stepY = parseInt((secondRow-firstRow)/steps);
        col1 = firstColumn;
        ro1 = firstRow;
        for (i = 1; i &lt;= steps; i++) {
            col2 = col1 + stepX;
            ro2 = ro1 + stepY;
            line = presenter.$view.find('#line_'+col1+'_'+ro1+'_'+col2+'_'+ro2);
            if (line.length &lt;= 0) {
                return false
            }
            col1 = col2;
            ro1 = ro2;
        }
        return true;
    };

    presenter.countDrawnLines = function() {
        var numberOfLines = presenter.$view.find('.line').not('.nonremovable').length;
        return numberOfLines;
    };

    presenter.allLinesDrawn = function() {
        var answer = true;
        if (presenter.activity &amp;&amp; !presenter.error) {
            var i, j, line, line2, color, counter = 0, errorCounter = presenter.$view.find('.line').not('.nonremovable').length, numberOfAnswers = presenter.AnswerLines.length;
            for (i = 0; i &lt;= numberOfAnswers; i++) {
                line = presenter.$view.find('#'+presenter.AnswerLines[i]);
                if (line.length &gt; 0) {
                    counter++;
                    errorCounter--;
                }
            }
            if (counter != numberOfAnswers || errorCounter != 0)
                answer = false;
        };
        return answer;
    };
    presenter.setDrawMode = function() {
        presenter.drawingMode = true;
        presenter.$view.find('.figure').addClass('drawing_mode');
        presenter.$view.find('.figure').removeClass('coloring_mode');
    };
    presenter.setColorMode = function() {
        if (presenter.coloring &amp;&amp; (!presenter.blockColoring || presenter.allLinesDrawn() || !presenter.drawingMode)) {
            presenter.drawingMode = false;
            presenter.$view.find('.figure').addClass('coloring_mode');
            presenter.$view.find('.selected').removeClass('selected');
            presenter.$view.find('.figure').removeClass('drawing_mode');
            presenter.selected.isSelected = false;
        }
    };
    presenter.setColor = function(color) {
        presenter.isEraser = false;
        var color = validateColor(color,false,false);
        if (color != false)
            presenter.currentColor = color;
    };
    presenter.setEraserOn = function() {
        presenter.isEraser = true;
        presenter.currentColor = [255,255,255,0];
    };

    presenter.isAttempted = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        return presenter.isStarted;
    };

    presenter.isAllOK = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        return ((presenter.getScore() == presenter.getMaxScore()) &amp;&amp; (presenter.getErrorCount() === 0));
    };

    presenter.disable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();

        if (!(presenter.$view.find('.disabled').length &gt; 0)) {
            presenter.disabled = true;
            presenter.$view.find('.figure').addClass('disabled');
            presenter.$view.find('.chart').addClass('disabled');
        }
    };

    presenter.enable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();

        presenter.disabled = false;
        presenter.$view.find('.disabled').removeClass('disabled');
    };

    presenter.hide = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        if (presenter.coloring) presenter.$view.find('.canvas').css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isVisible ? presenter.show() : presenter.hide();
    };

    function validateRadius(radius){
        if (radius == '' || radius == 0) {
            return 5;
        } else if (radius &gt; 0) {
            return parseInt(radius)
        } else {
            presenter.error = 'radius';
            return 5;
        }
    };
    function validateStartingColor(params){
        var answer = [], point;
        if (params == '' || params == undefined) return '';
        var pointsData = Helpers.splitLines(params);
        var re = /^\d+;\d+;\d{1,3} \d{1,3} \d{1,3}$/;
        var testing;
        $.each(pointsData, function() {
            if (this != '') {
                testing = re.test(this);
                if (!testing) {
                    presenter.error = 'startingcolor';
                    return '';
                }
                point = this.split(';');
                if (isNaN(point[0]) || isNaN(point[1]) || point[0] &lt; 0 || point[1] &lt; 0 || point[0] &gt; presenter.canvasWidth || point[1] &gt; presenter.canvasHeight) {
                    presenter.error = 'startingcolor';
                    return '';
                }
                colors = validateColor(point[2],false,true);
                answer.push([parseInt(point[0]),parseInt(point[1]),colors[0],colors[1],colors[2],colors[3]]);
            }
        });
        return answer;
    }
    function validateLines(lines, draw, coloranswers){
        presenter.tmpLine = [];
        if (lines == '' || lines == undefined) return true;
        var Lines = Helpers.splitLines(lines);
        var point, x, y, tmpPoint1, tmpPoint2, nonremovable;
        var re = /^\d+;\d+-\d+;\d+$/;
        var re2 = /^\d+;\d+-\d+;\d+\*$/;
        var testing;
        $.each(Lines, function() {
            if (this != '') {
                nonremovable = true;
                testing = re.test(this);
                if (draw &amp;&amp; !testing)
                    testing = re2.test(this);
                if (!testing) {
                    draw ? presenter.error = 'deflines' : presenter.error = 'answerlines';
                    return false;
                }
                splittedByDash = this.split('-');
                point = splittedByDash[0].split(';');
                if (isNaN(point[0]) || isNaN(point[1]) || (point[0] &gt; presenter.pointsX+1 &amp;&amp; !presenter.grid3D) || (point[0] &gt; 2*presenter.pointsX+1 &amp;&amp; presenter.grid3D) || point[1] &gt; presenter.pointsY+1) {
                    draw ? presenter.error = 'deflines' : presenter.error = 'answerlines';
                    return false;
                }
                if (presenter.grid3D &amp;&amp; (parseInt(point[0]) + parseInt(point[1])) % 2 == 0) {
                    draw ? presenter.error = 'deflines' : presenter.error = 'answerlines';
                    return false;
                }
                x = countX(point[0]); y = countY(point[1]);
                tmpPoint1 = new Point(parseInt(point[1]), parseInt(point[0]), x, y);
                point = splittedByDash[1].split(';');
                if (point[1].slice(-1) == '*' &amp;&amp; draw) {
                    nonremovable = false;
                    point[1] = point[1].substr(0,point[1].length-1);
                };
                if (isNaN(point[0]) || isNaN(point[1]) || (point[0] &gt; presenter.pointsX+1 &amp;&amp; !presenter.grid3D) || point[1] &gt; presenter.pointsY+1 || (point[0] &gt; 2*presenter.pointsX+1 &amp;&amp; presenter.grid3D)) {
                    draw ? presenter.error = 'deflines' : presenter.error = 'answerlines';
                    return false;
                }
                if (presenter.grid3D &amp;&amp; (parseInt(point[0]) + parseInt(point[1])) % 2 == 0) {
                    draw ? presenter.error = 'deflines' : presenter.error = 'answerlines';
                    return false;
                }
                y = countY(point[1]);
                x = countX(point[0]);
                tmpPoint2 = new Point(parseInt(point[1]), parseInt(point[0]), x, y);
                presenter.drawLine(tmpPoint1, tmpPoint2, nonremovable,draw, coloranswers,false);
            }
        });
    }
    function validateAnswersColor(list) {
        presenter.answersColors = [];
        var points, color = [], tmpPoint, helpString, tmpData, i, j, k;
        var re = /^(\d+;\d+-)+(\d+;\d+)$/;
        var re2 = /^\d+;\d+;\d{1,3} \d{1,3} \d{1,3}$/;
        var testing;
        for (i = 0; i &lt; list.length; i++) {
            points = [];
            if (tmpPoint == '') {
                presenter.error = 'answerfigure';
                return false;
            }
            testing = re.test(list[i]['Figure']);
            if (!testing) {
                presenter.error = 'answerfigure';
                return false;
            }
            tmpPoint = list[i]['Figure'].split('-');
            for (j=0; j &lt; tmpPoint.length-1; j++) {
                helpString = tmpPoint[j] +'-'+tmpPoint[j+1];
                validateLines(helpString,false, true);
                for (k=0; k &lt; presenter.tmpLine.length;k++)
                    points.push(presenter.tmpLine[k]);
            }
            if (list[i]['Color'] != '') {
                testing = re2.test(list[i]['Color']);
                if (!testing) {
                    presenter.error = 'colorerror';
                    return false;
                }
                color = validateStartingColor(list[i]['Color'])[0]
            }
            tmpData = {
                lines: points,
                x: color[0],
                y: color[1],
                color: color[2]+' '+color[3]+' '+color[4]+' '+color[5]
            };
            if (tmpPoint[0] != tmpPoint[j]) {
                presenter.error = 'answerfigure';
                return false;
            }
            presenter.answersColors.push(tmpData);
        }
    }
    function validateColor(color,isDefault,isEditor){
        var rgb = color.split(' ');
        if (isNaN(rgb[0]) || isNaN(rgb[1]) || isNaN(rgb[2]) || rgb[0] &lt; 0 || rgb[0] &gt; 255 || rgb[1]&lt;0 || rgb[1]&gt;255 || rgb[2]&lt;0 || rgb[2]&gt;255 || rgb.length != 3) {
            if (isEditor)
                (isDefault) ? (presenter.error = 'defcolorerror') : (presenter.error = 'colorerror');
            else
                return false;
        };
        rgb.push('255');
        return rgb;
    };
    function Point (row, column, x, y) {
        this.row = row;
        this.column = column;
        this.x = x;
        this.y = y;
    }
    presenter.selected = {
        row: 0,
        column: 0,
        x: 0,
        y: 0,
        isSelected: false
    }
    presenter.drawingPoint = {
        row: 0,
        column: 0,
        x: 0,
        y: 0,
        isDown: false
    }
    var gcd = function(a, b) {
        if (!b) {
            return a;
        }
        return gcd(b, a % b);
    }
    var countX = function(column) {
        if (presenter.grid3D) return (0.5 * column * presenter.grid)
        else return ((parseInt(column)-0.5) * presenter.grid);
    };
    var countY = function(row) {
        if (presenter.grid3D) return ((1/3*(parseInt(row)-1) + 0.5) * presenter.grid)
        else return ((parseInt(row)-0.5) * presenter.grid);
    };

    function uniq(a) {
        return a.filter(function (value, index, array) {
            return array.indexOf(value) === index;
        });
    }

    function drawOneLine(point1, point2, nonremovable) {
        var newLine = document.createElementNS("http://www.w3.org/2000/svg", 'line');
        newLine.setAttribute("y1",point1.y);
        newLine.setAttribute("x1",point1.x);
        newLine.setAttribute("y2",point2.y);
        newLine.setAttribute("x2",point2.x);
        newLine.setAttribute("id",'line_'+(point1.column)+'_'+(point1.row)+'_'+(point2.column)+'_'+(point2.row))
        nonremovable ? newLine.setAttribute("class","line nonremovable") : newLine.setAttribute("class","line");
        var firstPoint = presenter.$view.find('.point')[0];
        presenter.$view.find('.chart')[0].insertBefore(newLine,firstPoint);
        if (!presenter.isShowAnswersActive &amp;&amp; !presenter.isGradualShowAnswersActive &amp;&amp; presenter.coloring) drawLineOnCanvas(point1.x,point1.y,point2.x,point2.y);
    }

    function drawTempLine(point1, point2) {
        line = presenter.$view.find('.templine');
        if (line.length &gt; 0) {
            line.attr('y1',point1.y);
            line.attr('x1',point1.x);
            line.attr('y2',point2.y);
            line.attr('x2',point2.x);
        } else {
            var newLine = document.createElementNS("http://www.w3.org/2000/svg", 'line');
            newLine.setAttribute("y1",point1.y);
            newLine.setAttribute("x1",point1.x);
            newLine.setAttribute("y2",point2.y);
            newLine.setAttribute("x2",point2.x);
            newLine.setAttribute("class","templine");
            var firstPoint = presenter.$view.find('.point')[0];
            presenter.$view.find('.chart')[0].insertBefore(newLine,firstPoint);
        }
    }
    function drawLineOnCanvas(x1,y1,x2,y2) {
        presenter.ctx.beginPath();
        presenter.ctx.moveTo(x1,y1);
        presenter.ctx.lineTo(x2,y2);
        presenter.ctx.closePath();
        presenter.ctx.strokeStyle="rgba(5,5,5,0.8)";
        presenter.ctx.stroke();
    }
    presenter.redrawCanvas = function(showAnswers) {
        //	presenter.canvas.getContext('2d').clearRect(0,0,presenter.canvasWidth,presenter.canvasHeight);
        presenter.canvas.width += 0;
        var line = presenter.$view.find('.line');
        var i, x1, y1, x2, y2, tmpColor, tmpColor2, tmpPoint;
        var indexes = new Array();
        for(i = 0; i &lt; line.length; i++) {
            indexes = line[i].id.split('_');
            x1 = countX(indexes[1]); y1 = countY(indexes[2]);
            x2 = countX(indexes[3]); y2 = countY(indexes[4]);
            drawLineOnCanvas(x1,y1,x2,y2);
        }
        if (showAnswers) {
            presenter.fillFiguresWithAnswerColor();
        } else {
            for(i = 0; i &lt; presenter.coloredAreas.length; i++) {
                tmpPoint = [presenter.coloredAreas[i][0],presenter.coloredAreas[i][1]];
                tmpColor = [presenter.coloredAreas[i][2],presenter.coloredAreas[i][3],presenter.coloredAreas[i][4],presenter.coloredAreas[i][5]];
                tmpColor2 = getClickedAreaColor(tmpPoint[0],tmpPoint[1]);
                if (tmpColor2[0] != tmpColor[0] || tmpColor2[1] != tmpColor[1] || tmpColor2[2] != tmpColor2[2] || tmpColor2[3] != tmpColor[3]) {
                    floodFill(tmpPoint,tmpColor);
                } else
                    presenter.coloredAreas.splice(i,1);
            }
            checkColors();
        }
    }
    presenter.drawLine = function(point1, point2, nonremovable, draw, coloranswers, trigger){
        var firstPoint, secondPoint, column1, row1, row, column, column2, row2, tmpPoint;
        var x1, x2, y1, y2, x1N, y1N, x2N, y2N, x, y;
        var i, line, counter = 0;
        if (point1.column &lt; point2.column || (point1.column == point2.column &amp;&amp; point1.row &lt; point2.row)) {
            firstPoint = point1;
            secondPoint = point2;
        }  else {
            firstPoint = point2;
            secondPoint = point1;
        }
        var steps = Math.abs(gcd(secondPoint.row-firstPoint.row, secondPoint.column-firstPoint.column));
        if (presenter.grid3D &amp;&amp; (secondPoint.row-firstPoint.row+secondPoint.column-firstPoint.column)/steps %2 != 0) steps = 0.5 * steps;
        var stepX = parseInt((secondPoint.column-firstPoint.column)/steps);
        var	stepY = parseInt((secondPoint.row-firstPoint.row)/steps);
        column1 = firstPoint.column;
        row1 = firstPoint.row;
        for (i = 1; i &lt;= steps; i++) {
            column2 = column1 + stepX;
            row2 = row1 + stepY;
            line = presenter.$view.find('#line_'+column1+'_'+row1+'_'+column2+'_'+row2);
            if (line.length &gt; 0)
                counter++;
            if (!draw &amp;&amp; (line.length &gt; 0) &amp;&amp; line.attr('class') == 'line nonremovable' &amp;&amp; !coloranswers) {
                presenter.error = 'nonremanswer';
                return false;
            };
            column1 = column2;
            row1 = row2;
        }
        column1 = firstPoint.column;
        row1 = firstPoint.row;
        if (steps == counter &amp;&amp; draw) {
            for (i = 1; i &lt;= steps; i++) {
                column2 = column1 + stepX;
                row2 = row1 + stepY;
                line = presenter.$view.find('#line_'+column1+'_'+row1+'_'+column2+'_'+row2);
                if (line.attr('class') != 'line nonremovable')
                    line.remove();
                column1 = column2;
                row1 = row2;

            }
            var item = 'line_'+point1.column+'_'+point1.row+'_'+point2.column+'_'+point2.row;
            var value = 0;
            var score = 0;
            if (trigger) presenter.triggerEvent(item,value,score);
        } else {
            var score = 1;
            for (i = 1; i &lt;= steps; i++) {
                column = firstPoint.column + stepX;
                row = firstPoint.row + stepY;
                if (presenter.grid3D) {
                    x = firstPoint.x + stepX * 0.5 * presenter.grid;
                    y = firstPoint.y + stepY * 1/3 * presenter.grid;
                } else {
                    x = firstPoint.x + stepX * presenter.grid;
                    y = firstPoint.y + stepY * presenter.grid;
                }
                tmpPoint = new Point(row, column, x, y);
                line = 'line_'+firstPoint.column+'_'+firstPoint.row+'_'+tmpPoint.column+'_'+tmpPoint.row;
                if (presenter.$view.find('#'+line).length &lt;= 0 &amp;&amp; draw) {
                    drawOneLine(firstPoint, tmpPoint, nonremovable);

                    if (score == 1 &amp;&amp; ($.inArray(line,presenter.AnswerLines) == -1))
                        score = 0;
                }
                if (!draw &amp;&amp; !((presenter.$view.find('#'+line).length &gt; 0) &amp;&amp; presenter.$view.find('#'+line).attr('class') == 'line nonremovable'))
                    presenter.AnswerLines.push('line_'+firstPoint.column+'_'+firstPoint.row+'_'+tmpPoint.column+'_'+tmpPoint.row);
                if (coloranswers)
                    presenter.tmpLine.push('line_'+firstPoint.column+'_'+firstPoint.row+'_'+tmpPoint.column+'_'+tmpPoint.row);
                firstPoint = tmpPoint;
            }
            var item = 'line_'+point1.column+'_'+point1.row+'_'+point2.column+'_'+point2.row;
            var value = 1;
            if (trigger) presenter.triggerEvent(item,value,score);
        }
    }

    presenter.initiate = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        presenter.figure = presenter.$view.find('.figure');
        presenter.setDrawMode();
        presenter.activity = ModelValidationUtils.validateBoolean(presenter.model['IsActivity']);
        presenter.disabled = ModelValidationUtils.validateBoolean(presenter.model['IsDisabled']);
        presenter.initDisabled = presenter.disabled;
        presenter.isVisible = ModelValidationUtils.validateBoolean(presenter.model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        presenter.grid3D = ModelValidationUtils.validateBoolean(presenter.model['3DGrid']);
        presenter.showGrid = ModelValidationUtils.validateBoolean(presenter.model['ShowGrid']);
        presenter.StartingLines = presenter.model['StartingLines'];
        presenter.Answer = presenter.model['Answer'];
        presenter.grid = parseInt(presenter.model['Grid']);
        if (!(presenter.grid &gt; 1))
            presenter.error = 'grid';
        presenter.radius = validateRadius(presenter.model['Radius']);
        presenter.coloring = ModelValidationUtils.validateBoolean(presenter.model['Coloring']);
        presenter.blockColoring = ModelValidationUtils.validateBoolean(presenter.model['BlockColoring']);
        presenter.answersColors = [];
        if (presenter.coloring &amp;&amp; !presenter.error) {
            presenter.defaultColor = validateColor(presenter.model['DefColor'],true,true);
            presenter.startingColors = validateStartingColor(presenter.model['StartingColors']);
        };
        return true;
    }

    presenter.drawGrid = function() {
        var Width = presenter.figure.width();
        var Height = presenter.figure.parent().height();
        var i, j, presentX, presentY;
        presenter.pointsX = parseInt((Width - presenter.grid) / presenter.grid);
        presenter.pointsY = parseInt((Height - presenter.grid) / presenter.grid);
        if (presenter.grid3D) {
            presenter.pointsY = parseInt(3*(Height - presenter.grid) / presenter.grid);
            Height = parseInt((presenter.pointsY / 3 + 1) * presenter.grid);
            presenter.pointsX = parseInt((Width - presenter.grid) / presenter.grid);
            Width = parseInt((presenter.pointsX + 1) * presenter.grid);
        } else {
            Width = (presenter.pointsX + 1) * presenter.grid;
            Height = (presenter.pointsY + 1) * presenter.grid;
        }
        presenter.figure.css({'width' : Width, 'height' : Height});
        var $svg = '&lt;svg height="'+Height+'" width="'+Width+'" version="1.1" xmlns:xlink="http://www.w3.org/2000/svg" class="chart"&gt;';
        points = '';
        for (i = 0; i &lt;= presenter.pointsX; i++) {
            presentX = (i + 0.5) * presenter.grid;
            if (presenter.showGrid &amp;&amp; !presenter.grid3D)
                $svg += '&lt;line id="j_'+(i+1)+'" class ="grid" y2="' + Height +'" x2="'+presentX+'" y1="0" x1="'+presentX+'"&gt;&lt;/line&gt;';
            if (presenter.showGrid &amp;&amp; presenter.grid3D) {
                if (presenter.pointsY % 2 == 0) {
                    $svg += '&lt;line id="j_'+(i+1)+'" class ="grid" y2="' + (Height - 5/6 * presenter.grid) +'" x2="'+presentX+'" y1="'+(5/6*presenter.grid)+'" x1="'+presentX+'"&gt;&lt;/line&gt;';
                } else {
                    $svg += '&lt;line id="j_'+(i+1)+'" class ="grid" y2="' + (Height - 1/2*presenter.grid) +'" x2="'+presentX+'" y1="'+(5/6*presenter.grid)+'" x1="'+presentX+'"&gt;&lt;/line&gt;';
                }
                if (presenter.grid3D &amp;&amp; i &lt; presenter.pointsX) {
                    if (presenter.pointsY % 2 == 0) {
                        $svg += '&lt;line id="j__'+(i+1)+'" class ="grid" y2="' + (Height - 1/2*presenter.grid) +'" x2="'+(presentX+0.5*presenter.grid)+'" y1="'+(0.5*presenter.grid)+'" x1="'+(presentX+0.5*presenter.grid)+'"&gt;&lt;/line&gt;';
                    } else {
                        $svg += '&lt;line id="j__'+(i+1)+'" class ="grid" y2="' + (Height - 5/6*presenter.grid) +'" x2="'+(presentX+0.5*presenter.grid)+'" y1="'+(0.5*presenter.grid)+'" x1="'+(presentX+0.5*presenter.grid)+'"&gt;&lt;/line&gt;';
                    }
                }
            }
            for (j = 0; j &lt;= presenter.pointsY; j++) {
                if (presenter.grid3D) {
                    ((j % 2) == 0) ? (presentX = (1 + i)* presenter.grid) : (presentX = (0.5 + i)* presenter.grid);
                }
                (presenter.grid3D) ? (presentY = (1/3 * j + 0.5) * presenter.grid) : (presentY = (0.5 + j)* presenter.grid);
                if (!presenter.grid3D) {
                    points += '&lt;circle class="point" row="'+(j+1)+'" column ="'+(i+1)+'" r="'+presenter.radius+'" cy="'+presentY+'" cx="'+presentX+'"&gt;&lt;/circle&gt;';
                } else if (!(i == presenter.pointsX &amp;&amp; (j % 2) == 0)) {
                    points += '&lt;circle class="point" row="'+(j+1)+'" column ="'+ ((j%2==0) ? (2*(i+1)) : (2*(i+1)-1)) +'" r="'+presenter.radius+'" cy="'+presentY+'" cx="'+presentX+'"&gt;&lt;/circle&gt;';
                }
                if (i == 0 &amp;&amp; presenter.showGrid &amp;&amp; !presenter.grid3D) {
                    $svg += '&lt;line id="i_'+(j+1)+'" class ="grid" y2="'+presentY+'" x2="' + Width +'" y1="'+presentY+'" x1="0"&gt;&lt;/line&gt;';
                }
            }
        }
        if (presenter.showGrid &amp;&amp; presenter.grid3D) {
            for (i = 1; i &lt;= (presenter.pointsY/2); i++) {
                $svg += '&lt;line id="i_'+(i)+'" class ="grid" x1="'+(0.5*presenter.grid)+'" y1="'+((1/6+2/3*i)*presenter.grid)+'" x2="' + (0.5 * presenter.grid * (Math.min((presenter.pointsY-2*i+1),(2*presenter.pointsX)) + 1)) +'" y2="'+(1/3 * presenter.grid * (Math.min((presenter.pointsY-2*i+1),(2*presenter.pointsX)) + 0.5 + 2*i))+'"&gt;&lt;/line&gt;';
                $svg += '&lt;line id="i_'+(i)+'_" class ="grid" x1="'+(0.5*presenter.grid)+'" y1="'+((1/6+2/3*i)*presenter.grid)+'" x2="' + (0.5 * presenter.grid * (Math.min(2*i-1, 2*presenter.pointsX) + 1)) +'" y2="'+(1/3 * presenter.grid * (0.5 + 2*i - Math.min(2*i-1, 2*presenter.pointsX)))+'"&gt;&lt;/line&gt;';
            }
            for (i = 1; i &lt;= presenter.pointsX; i++) {
                $svg += '&lt;line id="i__'+(i)+'" class ="grid" x1="'+(i*presenter.grid)+'" y1="'+(0.5*presenter.grid)+'" x2="'+ (0.5 * presenter.grid * (Math.min(2*(presenter.pointsX-i)+1, presenter.pointsY) + 2*i)) +'" y2="'+ (presenter.grid * (0.5 + 1/3* Math.min(2*(presenter.pointsX-i)+1, presenter.pointsY))) +'"&gt;&lt;/line&gt;';
                if (presenter.pointsY % 2 == 0) {
                    $svg += '&lt;line id="i__'+(i)+'_" class ="grid" x1="'+(i*presenter.grid)+'" y1="'+((presenter.pointsY/3+0.5)*presenter.grid)+'" x2="'+  (0.5 * presenter.grid * (Math.min(2*(presenter.pointsX-i)+1, presenter.pointsY) + 2*i)) +'" y2="'+ (1/3 * presenter.grid * (presenter.pointsY + 1.5 - Math.min(2*(presenter.pointsX-i)+1, presenter.pointsY))) +'"&gt;&lt;/line&gt;';
                } else {
                    $svg += '&lt;line id="i__'+(i)+'_" class ="grid" x1="'+((i-0.5)*presenter.grid)+'" y1="'+((presenter.pointsY/3+0.5)*presenter.grid)+'" x2 ="'+ (0.5 * presenter.grid * (Math.min(2*(presenter.pointsX-i)+1, presenter.pointsY-1) + 2*i)) +'" y2="' + (1/3 * presenter.grid * (presenter.pointsY + 1.5 - Math.min(2*(presenter.pointsX-i+1),presenter.pointsY))) +'"&gt;&lt;/line&gt;';
                }
            }
        }
        $svg += points;
        $svg += '&lt;/svg&gt;';
        presenter.figure.prepend($svg);
        var canvasElement = $('&lt;canvas&gt;&lt;/canvas&gt;');
        presenter.ctx = canvasElement[0].getContext('2d');
        canvasElement.attr('width', Width);
        canvasElement.attr('height', Height);
        canvasElement.attr('class', 'canvas');
        presenter.canvasWidth = Width;
        presenter.canvasHeight = Height;
        presenter.canvas = canvasElement[0];
        if (presenter.coloring) presenter.figure.prepend(canvasElement);
        //double canvas fix
        if (presenter.coloring &amp;&amp; MobileUtils.isAndroidWebBrowser(window.navigator.userAgent)) {
            var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
            if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {
                presenter.$view.find('.canvas').parents("*").css("overflow", "visible");
            }
        }
    };

    presenter.run = function(view, model){
        var row, column, x, y;
        var timeClick = true, abandon = false;
        presenter.initiate(view, model);
        presenter.coloredAreas = [];
        presenter.down = false;
        if (!presenter.error) {
            presenter.drawGrid();
            validateLines(presenter.StartingLines,true,false);
            presenter.updateVisibility();

            var coordinations = {x:0, y:0};
            var tmpColor, i;
            if (presenter.coloring &amp;&amp; presenter.startingColors != '') {
                for (i = 0; i &lt; presenter.startingColors.length; i++)
                    presenter.coloredAreas[i] = presenter.startingColors[i];
                presenter.redrawCanvas(false);
            }
            presenter.currentColor = presenter.defaultColor;
            if (presenter.disabled) presenter.disable();
            if (presenter.activity) {
                presenter.AnswerLines = new Array();
                validateLines(presenter.Answer,false,false);
                if (presenter.coloring &amp;&amp; !presenter.error)
                    if (presenter.model['AnswerColors'].length != 1 || presenter.model['AnswerColors'][0]['Figure'] != '' || presenter.model['AnswerColors'][0]['Color'] != '')
                        validateAnswersColor(presenter.model['AnswerColors']);
                // delete duplicates from AnswerLines
                presenter.AnswerLines = uniq(presenter.AnswerLines);
            }
        }
        if (presenter.error) {
            presenter.figure.addClass(presenter.error);
            presenter.$view.find('.chart').remove();
            presenter.$view.find('.canvas').remove();
        };
        var point1, point2;
        presenter.$view.find('.point').on('mouseup touchend', function(e){
            if (!presenter.isErrorMode
                &amp;&amp; !presenter.disabled
                &amp;&amp; !presenter.isShowAnswersActive
                &amp;&amp; !presenter.isGradualShowAnswersActive
                &amp;&amp; presenter.drawingMode
                &amp;&amp; timeClick
            ) {
                e.stopPropagation();
                e.preventDefault();
                abandon = false;
                if (e.type != 'mouseup') timeClick = false;
                setTimeout(function(){timeClick = true;},400);
                if (e.type == 'mouseup') {
                    row = parseInt($(this).attr('row'),10);
                    column = parseInt($(this).attr('column'),10);
                    x = parseInt($(this).attr('cx'),10);
                    y = parseInt($(this).attr('cy'),10);
                } else {
                    row = findClosestPoint(presenter.mouseSX,presenter.mouseSY).row;
                    column = findClosestPoint(presenter.mouseSX,presenter.mouseSY).column;
                    x = countX(column);
                    y = countY(row);
                    if ((presenter.grid3D &amp;&amp; (column + row) % 2 == 0) || Math.abs(x-presenter.mouseSX) + Math.abs(y-presenter.mouseSY) &gt; 30 || row &lt; 1 || column &lt; 1 || row &gt; presenter.pointsY + 1 || (presenter.grid3D &amp;&amp; column &gt; 2*(presenter.pointsX)+1) || (!presenter.grid3D &amp;&amp; column &gt; presenter.pointsX + 1))
                        abandon = true;
                }
                if (!presenter.selected.isSelected &amp;&amp; !abandon) {
                    if (presenter.drawingPoint.row == row &amp;&amp; presenter.drawingPoint.column == column) {
                        presenter.selected.row = row;
                        presenter.selected.column = column;
                        presenter.selected.x = x;
                        presenter.selected.y = y;
                        presenter.selected.isSelected = true;
                        $(this).addClass('selected');
                    } else {
                        point1 = new Point(presenter.drawingPoint.row, presenter.drawingPoint.column, presenter.drawingPoint.x, presenter.drawingPoint.y);
                        point2 = new Point(row, column, x, y);
                        presenter.drawLine(point1,point2,false,true,false,true);
                        presenter.isStarted = true;
                        if (presenter.coloring) presenter.redrawCanvas(false);
                        //if blockColoring mode check if is OK
                        if (presenter.activity &amp;&amp; presenter.allLinesDrawn()) {
                            var item = 'lines';
                            var value = '';
                            var score = '';
                            presenter.triggerEvent(item,value,score);
                            if (presenter.blockColoring) presenter.setColorMode();
                        }
                    }
                } else if (!abandon) {
                    if (presenter.drawingPoint.row == row &amp;&amp; presenter.drawingPoint.column == column) {
                        if (presenter.selected.row != row || presenter.selected.column != column) {
                            point1 = new Point(presenter.selected.row, presenter.selected.column, presenter.selected.x, presenter.selected.y);
                            point2 = new Point(row, column, x, y);
                            presenter.drawLine(point1,point2,false,true,false,true);
                            presenter.isStarted = true;
                            presenter.selected.isSelected = false;
                            presenter.$view.find('.point').removeClass('selected');
                            if (presenter.coloring) presenter.redrawCanvas(false);
                            //if blockColoring mode check if is OK
                            if (presenter.activity &amp;&amp; presenter.allLinesDrawn()) {
                                var item = 'lines';
                                var value = '';
                                var score = '';
                                presenter.triggerEvent(item,value,score);
                                if (presenter.blockColoring) presenter.setColorMode();
                            }
                        }
                        presenter.selected.isSelected = false;
                        presenter.$view.find('.point').removeClass('selected');
                    } else {
                        point1 = new Point(presenter.drawingPoint.row, presenter.drawingPoint.column, presenter.drawingPoint.x, presenter.drawingPoint.y);
                        point2 = new Point(row, column, x, y);
                        presenter.drawLine(point1,point2,false,true,false,true);
                        presenter.isStarted = true;
                        if (presenter.coloring) presenter.redrawCanvas(false);
                        //if blockColoring mode check if is OK
                        if (presenter.activity &amp;&amp; presenter.allLinesDrawn()) {
                            var item = 'lines';
                            var value = '';
                            var score = '';
                            presenter.triggerEvent(item,value,score);
                            if (presenter.blockColoring) presenter.setColorMode();
                        }
                    }
                }
            };
            presenter.$view.find('.templine').remove();
            presenter.drawingPoint.isDown = false;
        });
        presenter.$view.find('.point').on('mousedown touchstart', function(e){
            presenter.$view.find('.templine').remove();
            if (!presenter.isErrorMode
                &amp;&amp; !presenter.disabled
                &amp;&amp; !presenter.isShowAnswersActive
                &amp;&amp; !presenter.isGradualShowAnswersActive
                &amp;&amp; presenter.drawingMode
            ) {
                e.stopPropagation();
                e.preventDefault();
                presenter.drawingPoint.row = parseInt($(this).attr('row'),10);
                presenter.drawingPoint.column = parseInt($(this).attr('column'),10);
                presenter.drawingPoint.x = parseInt($(this).attr('cx'),10);
                presenter.drawingPoint.y = parseInt($(this).attr('cy'),10);
                presenter.drawingPoint.isDown = true;
                if (e.type == 'touchstart') {
                    presenter.mouseSX = presenter.drawingPoint.x;
                    presenter.mouseSY = presenter.drawingPoint.y;
                }
            };
        });
        presenter.$view.on('mousedown touchstart', function(e){
            e.stopPropagation();
            e.preventDefault();
            if (presenter.coloring &amp;&amp; !presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActiv &amp;&amp; !presenter.drawingMode) {
                coordinations.x = e.originalEvent.pageX || e.originalEvent.touches[0].pageX;
                coordinations.y = e.originalEvent.pageY || e.originalEvent.touches[0].pageY;
                presenter.mouseSX = parseInt(coordinations.x,10) - parseInt(presenter.figure.offset().left,10);
                presenter.mouseSY = parseInt(coordinations.y,10) - parseInt(presenter.figure.offset().top,10);
                var imgData = presenter.ctx.getImageData(0, 0, presenter.canvasWidth, presenter.canvasHeight);
                var myPoint = (presenter.mouseSX + presenter.mouseSY * presenter.canvasWidth) * 4;
                var color = getClickedAreaColor(presenter.mouseSX,presenter.mouseSY);
                var startingPixel = [presenter.mouseSX,presenter.mouseSY];
                tmpColor = getClickedAreaColor(startingPixel[0],startingPixel[1]);
                if ((presenter.currentColor[0] != tmpColor[0] || presenter.currentColor[1] != tmpColor[1] || presenter.currentColor[2] != tmpColor[2] || presenter.currentColor[3] != tmpColor[3]) &amp;&amp; (tmpColor[3] &lt; 10 || tmpColor[3] &gt; 245) &amp;&amp; presenter.mouseSX &gt; 0 &amp;&amp; presenter.mouseSY &gt; 0) {
                    presenter.isStarted = true;
                    floodFill(startingPixel,presenter.currentColor);
                    presenter.coloredAreas.push([startingPixel[0],startingPixel[1],presenter.currentColor[0],presenter.currentColor[1],presenter.currentColor[2],presenter.currentColor[3]]);
                    // checking if this area was not colored previously
                    checkColors();
                    var item = startingPixel[0]+' '+startingPixel[1];
                    var value = presenter.currentColor.join(" ");
                    var score = '';
                    presenter.triggerEvent(item,value,score);
                }
            }
        });
        presenter.$view.on('mousemove touchmove', function(e){
            e.stopPropagation();
            e.preventDefault();
            var scaleInfo = presenter.playerController.getScaleInformation()
            if (!presenter.isErrorMode
                &amp;&amp; !presenter.disabled
                &amp;&amp; !presenter.isShowAnswersActive
                &amp;&amp; !presenter.isGradualShowAnswersActive
                &amp;&amp; presenter.drawingMode
                &amp;&amp; presenter.drawingPoint.isDown
            ) {
                point1 = new Point(presenter.drawingPoint.row, presenter.drawingPoint.column, presenter.drawingPoint.x, presenter.drawingPoint.y);
                if (e.type == 'mousemove') {
                    coordinations.x = e.originalEvent.pageX/scaleInfo.scaleX;
                    coordinations.y = e.originalEvent.pageY/scaleInfo.scaleY;
                } else {
                    coordinations.x = e.originalEvent.touches[0].pageX/scaleInfo.scaleX;
                    coordinations.y = e.originalEvent.touches[0].pageY/scaleInfo.scaleY;
                };
                presenter.mouseSX = parseInt(coordinations.x,10) - parseInt(presenter.figure.offset().left/scaleInfo.scaleX,10);
                presenter.mouseSY = parseInt(coordinations.y,10) - parseInt(presenter.figure.offset().top/scaleInfo.scaleY,10);
                if (presenter.mouseSX &lt; 0 || presenter.mouseSX &gt; presenter.figure.width() || presenter.mouseSY &lt; 0 || presenter.mouseSY &gt; presenter.figure.height()) {
                    presenter.drawingPoint.isDown = false;
                    presenter.$view.find('.templine').remove();
                } else {
                    point2 = new Point(0, 0, presenter.mouseSX, presenter.mouseSY);
                    drawTempLine(point1, point2);
                }
            }
        });
        presenter.$view.on('mouseup mouseleave touchend touchleave', function(e){
            presenter.drawingPoint.isDown = false;
            presenter.$view.find('.templine').remove();
        });
        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        for (let i = 0; i &lt; events.length; i++) {
            presenter.eventBus.addEventListener(events[i], this);
        }
    }
    function checkColors() {
        for(var i = 0; i &lt; presenter.coloredAreas.length; i++) {
            var tmpColor = getClickedAreaColor(presenter.coloredAreas[i][0],presenter.coloredAreas[i][1]);
            if (tmpColor[0] != presenter.coloredAreas[i][2] || tmpColor[1] != presenter.coloredAreas[i][3] || tmpColor[2] != presenter.coloredAreas[i][4] || tmpColor[3] != presenter.coloredAreas[i][5])
                presenter.coloredAreas.splice(i,1);
        }
    }

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };
    presenter.createPreview = function(view, model){
        presenter.initiate(view, model);
        presenter.coloredAreas = [];
        var coordinations = {x:0, y:0};
        if (!presenter.error) {
            presenter.drawGrid();
            validateLines(presenter.StartingLines,true,false);
            if (presenter.activity) {
                presenter.AnswerLines = new Array();
                validateLines(presenter.Answer,false,false);
                if (presenter.coloring &amp;&amp; !presenter.error)
                    if (presenter.model['AnswerColors'].length != 1 || presenter.model['AnswerColors'][0]['Figure'] != '' || presenter.model['AnswerColors'][0]['Color'] != '')
                        validateAnswersColor(presenter.model['AnswerColors']);
            }

            if (presenter.disabled) presenter.disable();
            if (presenter.coloring &amp;&amp; presenter.startingColors != '') {
                presenter.coloredAreas = presenter.startingColors;
                presenter.redrawCanvas(false);
            }
            presenter.showAnswersInEditor = ModelValidationUtils.validateBoolean(presenter.model['ShowAns']);
            if (presenter.activity &amp;&amp; presenter.showAnswersInEditor)
                presenter.showAnswers();
            if (presenter.coloring) {
                var coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
                    xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                    yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;');
                coordinatesContainer.addClass('coordinates');
                coordinatesContainer.append(xContainer).append(yContainer);
                presenter.figure.append(coordinatesContainer);

                function setCalculatedPosition(e) {
                    coordinations.x = e.originalEvent.pageX || e.originalEvent.touches[0].pageX;
                    coordinations.y = e.originalEvent.pageY || e.originalEvent.touches[0].pageY;
                    presenter.mouseSX = parseInt(coordinations.x,10) - parseInt(presenter.figure.offset().left,10);
                    presenter.mouseSY = parseInt(coordinations.y,10) - parseInt(presenter.figure.offset().top,10);
                    xContainer.find('.value').html(presenter.mouseSX);
                    yContainer.find('.value').html(presenter.mouseSY);
                }

                var doesElementExist = function() {
                    var $moduleSelector = $('.moduleSelector[data-id="'+presenter.addonID+'"]');

                    if ($moduleSelector.length &gt; 0) {
                        $moduleSelector.on('mousemove', function(e) {
                            setCalculatedPosition(e);
                        });

                        clearInterval(interval);
                    }
                };

                var interval = setInterval(function() { doesElementExist(); }, 500);

                presenter.figure.on('mousemove', function(e) {
                    setCalculatedPosition(e);
                });
            }
        };
        if (presenter.error) {
            presenter.figure.addClass(presenter.error);
            presenter.$view.find('.chart').remove();
            presenter.$view.find('.canvas').remove();
        }
    }

    presenter.reset = function(resetOnlyWrong) {
        presenter.isErrorMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        if (presenter.coloring &amp;&amp; presenter.startingColors != '') {
            for (let i = 0; i &lt; presenter.startingColors.length; i++)
                presenter.coloredAreas[i] = presenter.startingColors[i];
        }

        if (resetOnlyWrong) {
            presenter.removeIncorrectLines();

            if (presenter.coloring) {
                var copyOfColoredAreas = JSON.parse(JSON.stringify(presenter.coloredAreas));
                presenter.coloredAreas = [];
                presenter.redrawCanvas(false);
                presenter.fillFiguresWithAnswerColor();
                presenter.coloredAreas = copyOfColoredAreas;
                var areasToRemove = presenter.getAreasToRemove();
                presenter.updateColoredAreas(areasToRemove);
            }
        } else {
            presenter.$view.find('.line').remove();
            presenter.coloredAreas = [];
        }

        validateLines(presenter.StartingLines,true,false);
        presenter.$view.find('.selected').removeClass('selected');
        presenter.selected.isSelected = false;
        presenter.disabled = presenter.initDisabled;
        if (presenter.disabled) presenter.disable()
        else presenter.enable();
        presenter.isVisible = presenter.initIsVisible;
        presenter.updateVisibility();
        if (presenter.coloring) presenter.redrawCanvas(false);
        presenter.isEraser = false;
        presenter.currentColor = presenter.defaultColor;

        if (resetOnlyWrong) {
            presenter.isStarted = true;
            if (presenter.blockColoring &amp;&amp; presenter.allLinesDrawn()) {
                presenter.setColorMode();
            } else {
                presenter.setDrawMode();
            }
        } else {
            presenter.isStarted = false;
            presenter.setDrawMode();
        }
    };

    presenter.removeIncorrectLines = function () {
        var lines = presenter.$view.find('.line').not('.nonremovable');
        for (let i = 0; i &lt; lines.length; i++) {
            if ($.inArray(lines[i].id, presenter.AnswerLines) === -1)
                presenter.$view.find('#' + lines[i].id).remove();
        }
    }

    presenter.fillFiguresWithAnswerColor = function () {
        for (let i = 0; i &lt; presenter.answersColors.length; i++) {
            let tmpPoint = [presenter.answersColors[i].x, presenter.answersColors[i].y];
            let tmpColor = presenter.answersColors[i].color.split(' ');
            let tmpColor2 = getClickedAreaColor(tmpPoint[0], tmpPoint[1]);
            if (tmpColor2[0] != tmpColor[0] || tmpColor2[1] != tmpColor[1] || tmpColor2[2] != tmpColor2[2] || tmpColor2[3] != tmpColor[3]) {
                floodFill(tmpPoint, tmpColor);
            }
        }
    }

    presenter.getAreasToRemove = function () {
        var figuresToRemove = [];
        for (let i = 0; i &lt; presenter.coloredAreas.length; i++) {
            let color = getClickedAreaColor(presenter.coloredAreas[i][0], presenter.coloredAreas[i][1]);
            if (color[0] != presenter.coloredAreas[i][2] || color[1] != presenter.coloredAreas[i][3] || color[2] != presenter.coloredAreas[i][4] || color[3] != presenter.coloredAreas[i][5]) {
                figuresToRemove.push(i);
            }
        }

        figuresToRemove.sort(function (x, y) {
            return y - x;
        });

        return figuresToRemove;
    }

    presenter.updateColoredAreas = function (figuresToRemove) {
        if (figuresToRemove.length === presenter.coloredAreas.length) {
            presenter.coloredAreas = [];
        } else {
            for (let i = 0; i &lt; figuresToRemove.length; i++) {
                presenter.coloredAreas.splice(figuresToRemove[i], 1);
            }
        }
    }

    presenter.getMaxScore = function(){
        if (!presenter.activity || presenter.error) {
            return 0;
        }

        let maxScore = presenter.AnswerLines.length;
        if (presenter.coloring) {
            maxScore = presenter.answersColors.length;
        }
        return maxScore;
    }

    presenter.getScore = function(){
        if (!presenter.activity || presenter.error || !presenter.isStarted) {
            return 0;
        }
        const wasShowAnswersActive = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        const previousGSAcounter = presenter.GSAcounter;
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        let i, j, $line, color, lineCounter, scoreCounter = 0;
        for (i = 0; i &lt;= presenter.AnswerLines.length; i++) {
            $line = presenter.$view.find('#'+presenter.AnswerLines[i]);
            if ($line.length &gt; 0) {
                scoreCounter++;
            }
        }
        if (presenter.coloring) {
            for (i = 0; i &lt; presenter.answersColors.length; i++) {
                lineCounter = 0;
                for (j = 0; j &lt; presenter.answersColors[i].lines.length; j++) {
                    $line = presenter.$view.find('#'+presenter.answersColors[i].lines[j]);
                    if ($line.length &gt; 0) {
                        lineCounter++;
                    }
                }
                color = getClickedAreaColor(presenter.answersColors[i].x,presenter.answersColors[i].y).join(" ");
                if (lineCounter == presenter.answersColors[i].lines.length &amp;&amp; color == presenter.answersColors[i].color) {
                    scoreCounter++;
                }
            }
        }

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        wasGradualShowAnswersActive &amp;&amp; gradualShowAnswers(previousGSAcounter);

        return scoreCounter;
    }

    presenter.getErrorCount = function(){
        if (!presenter.activity || presenter.error || !presenter.isStarted) {
            return 0;
        }
        const wasShowAnswersActive = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        const previousGSAcounter = presenter.GSAcounter;
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        let lineCounter, color, $line;
        let errorCounter = presenter.$view.find('.line').not('.nonremovable').length;
        for (let i = 0; i &lt;= presenter.AnswerLines.length; i++) {
            $line = presenter.$view.find('#'+presenter.AnswerLines[i]);
            if ($line.length &gt; 0) {
                errorCounter--;
            }
        }
        if (presenter.coloring) {
            for (let i = 0; i &lt; presenter.answersColors.length; i++) {
                lineCounter = 0;
                for (let j = 0; j &lt; presenter.answersColors[i].lines.length; j++) {
                    $line = presenter.$view.find('#'+presenter.answersColors[i].lines[j]);
                    if ($line.length &gt; 0) {
                        lineCounter++;
                    }
                }
                color = getClickedAreaColor(presenter.answersColors[i].x,presenter.answersColors[i].y).join(" ");
                if (color != presenter.answersColors[i].color &amp;&amp; color != '0 0 0 0' &amp;&amp; lineCounter == presenter.answersColors[i].lines.length) {
                    errorCounter++;
                }
            }
        }

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        wasGradualShowAnswersActive &amp;&amp; gradualShowAnswers(previousGSAcounter);

        return errorCounter;
    }

    presenter.setWorkMode = function() {
        presenter.isErrorMode = false;
        presenter.$view.find('.line').removeClass('correct');
        presenter.$view.find('.line').removeClass('wrong');
        presenter.$view.find('.icon-container').remove();
        presenter.$view.find('.figure').removeClass('correct');
        presenter.$view.find('.figure').removeClass('wrong');
    };

    presenter.setShowErrorsMode = function() {
        var i, j, line, color, lineCounter;
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isErrorMode = true;
        presenter.selected.isSelected = false;
        presenter.$view.find('.point').removeClass('selected');
        var Lines = presenter.$view.find('.line').not('.nonremovable');
        var i;
        if (presenter.activity &amp;&amp; !presenter.error &amp;&amp; presenter.isStarted) {
            for (i = 0; i &lt; Lines.length; i++) {;
                if ($.inArray(Lines[i].id,presenter.AnswerLines) != -1)
                    presenter.$view.find('#'+Lines[i].id).addClass('correct')
                else
                    presenter.$view.find('#'+Lines[i].id).addClass('wrong');
            }
        }
        if (presenter.activity &amp;&amp; !presenter.error &amp;&amp; presenter.isStarted &amp;&amp; presenter.coloring) {
            for (i = 0; i &lt; presenter.answersColors.length; i++) {
                lineCounter = 0;
                for (j = 0; j &lt; presenter.answersColors[i].lines.length; j++) {
                    line = presenter.$view.find('#'+presenter.answersColors[i].lines[j]);
                    if (line.length &gt; 0) lineCounter++;
                }
                color = getClickedAreaColor(presenter.answersColors[i].x,presenter.answersColors[i].y).join(" ");
                if (lineCounter == presenter.answersColors[i].lines.length &amp;&amp; color == presenter.answersColors[i].color)
                    presenter.displayIcon(presenter.answersColors[i].x,presenter.answersColors[i].y,true)
                else if (color != presenter.answersColors[i].color &amp;&amp; color != '0 0 0 0' &amp;&amp; lineCounter == presenter.answersColors[i].lines.length)
                    presenter.displayIcon(presenter.answersColors[i].x,presenter.answersColors[i].y,false);
            }
        }
    };
    presenter.displayIcon = function(x,y,isCorrect) {
        var iconContainer = $('&lt;div class="icon-container"&gt;&lt;/div&gt;');
        iconContainer.css({
            top: (y-5) + 'px',
            left: (x-5) + 'px'
        });
        iconContainer.addClass(isCorrect ? 'correct' : 'wrong');
        presenter.figure.append(iconContainer);
    }

    presenter.getState = function() {
        const wasShowAnswersActive = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        const previousGSAcounter = presenter.GSAcounter;
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        var Lines = presenter.$view.find('.line').not('.nonremovable');
        var LinesIds = new Array();
        for (let i = 0; i &lt; Lines.length; i++) {
            LinesIds.push(Lines[i].id);
        };
        const result = JSON.stringify({
            isStarted : presenter.isStarted,
            disabled : presenter.disabled,
            visible : presenter.isVisible,
            lines : LinesIds,
            eraser: presenter.isEraser,
            color: presenter.currentColor,
            coloredAreas: presenter.coloredAreas,
            mode: presenter.drawingMode
        })

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        wasGradualShowAnswersActive &amp;&amp; gradualShowAnswers(previousGSAcounter);
        return result;
    };

    presenter.setState = function(state) {
        var point1, point2, x, y, i;
        presenter.disabled = JSON.parse(state).disabled;
        presenter.isEraser = JSON.parse(state).eraser;
        presenter.currentColor = JSON.parse(state).color;
        presenter.coloredAreas = JSON.parse(state).coloredAreas;
        presenter.drawingMode = JSON.parse(state).mode;
        if (presenter.drawingMode)
            presenter.setDrawMode()
        else
            presenter.setColorMode();
        if (presenter.disabled)
            presenter.disable()
        else
            presenter.enable();
        presenter.isVisible = JSON.parse(state).visible;
        presenter.isStarted = JSON.parse(state).isStarted;
        presenter.updateVisibility();
        presenter.$view.find('.line').not('.nonremovable').remove();
        var LinesIds = JSON.parse(state).lines;
        var indexes = new Array();
        for (i = 0; i &lt; LinesIds.length; i++) {
            indexes = LinesIds[i].split('_');
            x = countX(indexes[1]); y = countY(indexes[2]);
            point1 = new Point(indexes[2], indexes[1], x, y);
            x = countX(indexes[3]); y = countY(indexes[4]);
            point2 = new Point(indexes[4], indexes[3], x, y);
            drawOneLine(point1,point2,false);
        }
        if (presenter.coloring) presenter.redrawCanvas(false);
    };

    presenter.showAnswers = function () {
        if (!presenter.activity) {
            return;
        }

        presenter.isErrorMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isShowAnswersActive = true;
        showAnswersTo();
    };

    presenter.gradualShowAnswers = function (eventData) {
        if (eventData.moduleID !== presenter.addonID) {
            return;
        }

        gradualShowAnswers(parseInt(eventData.item, 10));
    };

    function gradualShowAnswers(index) {
        if (!presenter.activity) {
            return;
        }

        presenter.isErrorMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isGradualShowAnswersActive = true;

        presenter.GSAcounter = index;
        showAnswersTo(presenter.GSAcounter + 1);
    }

    function showAnswersTo(index) {
        let $lines = presenter.$view.find('.line:not(.nonremovable)');
        presenter.savedLinesIdsSA = [];
        for (let i = 0; i &lt; $lines.length; i++) {
            presenter.savedLinesIdsSA.push($lines[i].id);
        }
        $lines.remove();
        drawLines(presenter.AnswerLines, index);
        $lines.addClass('show-answers');

        presenter.selected.isSelected = false;
        presenter.$view.find('.selected').removeClass('selected');

        presenter.coloring &amp;&amp; presenter.redrawCanvas(true);
    }

    function drawLines(linesIds, maxIndex) {
        if (maxIndex === undefined) {
            maxIndex = linesIds.length;
        }
        let indexes = [],
            x1, y1, point1,
            x2, y2, point2;
        for (let i = 0; i &lt; maxIndex; i++) {
            indexes = linesIds[i].split('_');

            x1 = countX(indexes[1]);
            y1 = countY(indexes[2]);
            point1 = new Point(indexes[2], indexes[1], x1, y1);

            x2 = countX(indexes[3]);
            y2 = countY(indexes[4]);
            point2 = new Point(indexes[4], indexes[3], x2, y2);

            drawOneLine(point1, point2,false);
        }
    }

    presenter.hideAnswers = function () {
        if (!presenter.activity
            || (!presenter.isShowAnswersActive
                &amp;&amp; !presenter.isGradualShowAnswersActive)) {
            return;
        }

        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
        presenter.GSAcounter = 0;

        presenter.$view.find('.line:not(.nonremovable)').remove();
        drawLines(presenter.savedLinesIdsSA);
        presenter.coloring &amp;&amp; presenter.redrawCanvas(false);
    };

    presenter.gradualHideAnswers = function() {
        presenter.hideAnswers();
    };

    presenter.getActivitiesCount = function () {
        return !presenter.activity ? 0 : presenter.AnswerLines.length;
    };

    function findClosestPoint(x,y) {
        if (presenter.grid3D) {
            var column = parseInt((x * 2 + 0.25 * presenter.grid)/presenter.grid);
            var row = parseInt(3*(y + 1/6 * presenter.grid - 0.5 * presenter.grid)/presenter.grid)+1;
        } else {
            var column = parseInt((x + 0.5 * presenter.grid)/presenter.grid);
            if (x - countX(column) &gt; 0.5 * presenter.grid) column++;
            var row = parseInt((y + 0.5 * presenter.grid)/presenter.grid);
            if (y - countY(row) &gt; 0.5 * presenter.grid) row++;
        }
        point = new Point(row, column, x, y);
        return point;
    }
    function floodFill(startingPixel,fillColor) {
        var imgData = presenter.ctx.getImageData(0, 0, presenter.canvasWidth, presenter.canvasHeight);
        var pixelStack = [];
        var newPos, x, y, pixelPos, reachLeft, reachRight;
        pixelStack.push(startingPixel);
        var start = getClickedAreaColor(startingPixel[0],startingPixel[1]);
        while(pixelStack.length) {
            newPos = pixelStack.pop();
            x = newPos[0];
            y = newPos[1];
            pixelPos = (y*presenter.canvasWidth + x) * 4;
            while(y-- &gt;= 0 &amp;&amp; matchStartColor(imgData,pixelPos,start)) {
                pixelPos -= presenter.canvasWidth * 4;
            }
            pixelPos += presenter.canvasWidth * 4;
            ++y;
            reachLeft = false;
            reachRight = false;
            while(y++ &lt; presenter.canvasHeight-1 &amp;&amp; matchStartColor(imgData,pixelPos,start)) {
                colorPixel(imgData,pixelPos,fillColor);
                if(x &gt; 0) {
                    if(matchStartColor(imgData,pixelPos - 4,start)) {
                        if(!reachLeft){
                            pixelStack.push([x - 1, y]);
                            reachLeft = true;
                        }
                    } else if(reachLeft) {
                        reachLeft = false;
                    }
                }
                if(x &lt; presenter.canvasWidth-1) {
                    if(matchStartColor(imgData,pixelPos + 4,start)) {
                        if(!reachRight) {
                            pixelStack.push([x + 1, y]);
                            reachRight = true;
                        }
                    } else if(reachRight) {
                        reachRight = false;
                    }
                }
                pixelPos += presenter.canvasWidth * 4;
            }
        }
        presenter.ctx.putImageData(imgData, 0, 0);
    }
    function matchStartColor(imgData,pixelPos,start) {
        var r = imgData.data[pixelPos];
        var g = imgData.data[pixelPos+1];
        var b = imgData.data[pixelPos+2];
        var a = imgData.data[pixelPos+3];
        return (r == start[0] &amp;&amp; g == start[1] &amp;&amp; b == start[2] &amp;&amp; a == start[3]);
    }
    function colorPixel(imgData,pixelPos,fillColor) {
        imgData.data[pixelPos] = fillColor[0];
        imgData.data[pixelPos+1] = fillColor[1];
        imgData.data[pixelPos+2] = fillColor[2];
        imgData.data[pixelPos+3] = fillColor[3];
    }
    function getClickedAreaColor(x, y) {
        var data = presenter.ctx.getImageData(x, y, 1, 1).data, color = [];
        for (var i = 0; i &lt; data.length; i++) {
            color.push(data[i]);
        }
        return color;
    }
    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };
    presenter.createEventData = function(item,value,score) {
        return {
            source : presenter.addonID,
            item : item,
            value : value,
            score : score
        };
    };
    presenter.triggerEvent = function(item, state, score) {
        var eventData = presenter.createEventData(item, state, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
        if (presenter.getScore() == presenter.getMaxScore() &amp;&amp; presenter.activity &amp;&amp; presenter.getErrorCount() == 0) {
            eventData = presenter.createEventData('all','','');
            presenter.eventBus.sendEvent('ValueChanged', eventData);
        }
    };
    return presenter;
}

AddonFigureDrawing_create.__supported_player_options__ = {
    resetInterfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="File_Sender" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="SourceId" nameLabel="File_Sender_property_source_id" type="string"/>
		<property isLocalized="true" name="SourceType" nameLabel="File_Sender_property_source_type" type="{File, Paragraph, Media Recorder}"/>
		<property isLocalized="true" name="ButtonText" nameLabel="File_Sender_property_button_text" type="string"/>
		<property isLocalized="true" name="DialogTitle" nameLabel="File_Sender_property_dialog_title" type="string"/>
		<property name="disableSendButton" nameLabel="File_Sender_property_disable_send_button" type="boolean"/>
	</model>
<css>.file-sender-input-wrapper {
    border: solid;
    overflow: hidden;
    position: relative;
    width: 100%;
    height: 100%;
}

.file-sender-input-file {
    width:100%;
    height:100%;
    opacity:0.0001;
    position: absolute;
    top: 0px;
}

.file-sender-input-label {
    width:100%;
    height:100%;
    position: absolute;
    top: 0px;
    word-break: break-all;
}

.file-sender-teacher-dialog-element {
    cursor: default;
    display: block;
    text-align: center;
}

.file-sender-send-file-button {
    cursor: default;
    display: block;
    text-align: center;
}

.file-sender-sent-file-reset {
    color: red;
    float: right;
    cursor: default;
}

.file-sender-sent-file-label {
    margin-bottom: 1em;
    cursor: default;
    overflow-y: clip;
}
</css><view>&lt;div class="file-sender-wrapper"&gt;
    &lt;form class="file-sender-input-wrapper"&gt;
        &lt;div class="file-sender-input-label"&gt;&lt;/div&gt;
        &lt;input class="file-sender-input-file" type="file" accept=".doc,.docx,.pdf,.odt,.jpg,.gif,.png,.html,.mp3,.wav"/&gt;
    &lt;/form&gt;
    &lt;div class="file-sender-sent-file"&gt;
        &lt;div class="file-sender-sent-file-reset"&gt;x&lt;/div&gt;
        &lt;div class="file-sender-sent-file-label"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="file-sender-send-file-button"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="file-sender-wrapper"&gt;
    &lt;form class="file-sender-input-wrapper"&gt;
        &lt;div class="file-sender-input-label"&gt;&lt;/div&gt;
        &lt;input class="file-sender-input-file" type="file"/&gt;
    &lt;/form&gt;
    &lt;div class="file-sender-send-file-button"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonFile_Sender_create() {
    var presenter = function() {};

    presenter.teachers = [];
    presenter.fileEndpointUrl = "";
    presenter.fileDownloadEndpointUrl = "";
    presenter.fileSenderPickRecipientAvailable = true;
    presenter.views = [];
    presenter.dialog = null;
    presenter.pageIndex = -1;
    presenter.$ICPage = null;
    presenter.contextLoaded = false;

    presenter.sentFileName = "";
    presenter.sentFileUrl = "";
    presenter.sentFileId = -1;
    presenter.isMessageSent = false;

    var currentScrollTop = 0;

    var LessonSendFileType = {
        'FILE': 0,
        'AUDIO': 1,
        'TEXT': 2,
    };

    var SOURCE_TYPES = {
        'FILE': 'File',
        'PARAGRAPH': 'Paragraph',
        'MEDIA_RECORDER': 'Media Recorder'
    };

    var ERROR_CODES = {
        V01: 'Source ID cannot be empty unless source type is set to File'
    };

    var inactiveClass = "file-sender-inactive";

    var sendFileEventType = "sendFile";
    var credentialsConfig = "same-origin";


    function presenterLogic(view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.$view = $(view);
        presenter.isPreview = isPreview;

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            presenter.$view.html(ERROR_CODES[presenter.configuration.errorCode]);
            return;
        }
        presenter.loadViewHandlers();
        presenter.attachHandlers();

        presenter.views.fileSentLabel.css('max-height', model['Height']+'px');
        presenter.views.form.css('height', model['Height']+'px');

        presenter.views.sendFileButton.html(presenter.configuration.buttonText);
        if (presenter.configuration.sourceType !== SOURCE_TYPES.FILE) {
            presenter.views.form.css('display', 'none');
            presenter.views.formLabel.css('display', 'none');
            presenter.views.form.css('width', model['Width']+'px');
            presenter.views.form.css('height', model['Height']+'px');
        } else {
            presenter.views.sendFileButton.css('width', model['Width']+'px');
            presenter.views.sendFileButton.css('height', model['Width']+'px');
        }

        presenter.hideSentFile();

        if (presenter.configuration.disableSendButton) {
            presenter.hideSendButton();
        } else {
            presenter.showSendButton();
        }

        if ((!isPreview) &amp;&amp; (!presenter.contextLoaded)) {
            presenter.$view.addClass(inactiveClass);
        }
    }

    presenter.show = function () {
        presenter.$view.css("display", "");
    }

    presenter.hide = function () {
        presenter.$view.css("display", "none");
    }

    presenter.validateModel = function (model) {
        var sourceType = model.SourceType;
        if (sourceType.length == 0) sourceType = SOURCE_TYPES.FILE;

        if (sourceType !== SOURCE_TYPES.FILE &amp;&amp; model.SourceId.length == 0) {
             return {isValid: false, errorCode: 'V01'};
        }

        return {
            ID: model.ID,
            isValid: true,
            sourceId: model.SourceId,
            sourceType: sourceType,
            buttonText: model.ButtonText,
            dialogTitle: model.DialogTitle,
            disableSendButton: ModelValidationUtils.validateBoolean(model['disableSendButton'])
        }
    }

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.pageIndex = presenter.playerController.getCurrentPageIndex();
        if (presenter.playerController.getRequestsConfig().shouldIncludeCredentials()) {
            credentialsConfig = "include";
        }

        presenter.loadContext(10);
    };

    presenter.loadContext = function(iterationsLeft) {
        //return true if context was loaded and false if it was not
        var context = presenter.playerController.getContextMetadata();
        if (context != null) {
            if ("teachers" in context) {
                presenter.teachers = context["teachers"];
            }
            if ("fileEndpointUrl" in context) {
                presenter.fileEndpointUrl = context["fileEndpointUrl"];
            }
            if ("fileDownloadEndpointUrl" in context) {
                presenter.fileDownloadEndpointUrl = context["fileDownloadEndpointUrl"];
            }
            if (context["fileSenderPickRecipientAvailable"] == false) {
                presenter.fileSenderPickRecipientAvailable = false;
            }
            if (presenter.fileEndpointUrl.length &gt; 0
                &amp;&amp; presenter.fileDownloadEndpointUrl.length &gt; 0) {
                presenter.contextLoaded = true;
                if (presenter.$view) {
                    presenter.$view.removeClass(inactiveClass);
                }
            }
        } else {
            if (iterationsLeft &gt; 0) {
                window.setTimeout(function(){presenter.loadContext(iterationsLeft-1)}, 500);
            }
        }
    };

    presenter.getState = function() {
            var state = {
                sentFileName: presenter.sentFileName,
                sentFileId: presenter.sentFileId,
                isMessageSent: presenter.isMessageSent
            };
            return JSON.stringify(state);
    }

    presenter.setState = function(state) {
        if (state == null || state.length == 0) return;
        var parsedState = JSON.parse(state);
        presenter.setSentFile(parsedState.sentFileName, parsedState.sentFileId);
        presenter.isMessageSent = parsedState.isMessageSent;
        if (parsedState.sentFileName.length &gt; 0 &amp;&amp; parsedState.sentFileId != -1) {
            presenter.showSentFile();
            if (parsedState.isMessageSent) {
                presenter.hideSendButton();
            } else {
                presenter.showSendButton();
            }
        }
    }

    presenter.fireSendFileEvent = function(fileID, targetID) {
        if (presenter.playerController &amp;&amp; !presenter.configuration.disableSendButton) {
            var fileType = LessonSendFileType.FILE; //FILE
            if (presenter.configuration.sourceType == SOURCE_TYPES.PARAGRAPH) {
                fileType = LessonSendFileType.TEXT;
            } else if (presenter.configuration.sourceType == SOURCE_TYPES.MEDIA_RECORDER) {
                fileType = LessonSendFileType.AUDIO;
            }
            var data = {
                teachers: [targetID],
                fileId: fileID,
                fileType: fileType
            };
            var jsonData = JSON.stringify(data);
            presenter.playerController.sendExternalEvent(sendFileEventType, jsonData);
            presenter.isMessageSent = true;
        } else {
            console.error("Cannot make a request: no player controller");
        }
    }

    presenter.loadViewHandlers = function() {
        presenter.views.wrapper = presenter.$view.find('.file-sender-wrapper');
        presenter.views.form = presenter.$view.find('.file-sender-input-wrapper');
        presenter.views.formInput = presenter.$view.find('.file-sender-input-file');
        presenter.views.formLabel = presenter.$view.find('.file-sender-input-label');
        presenter.views.sendFileButton = presenter.$view.find('.file-sender-send-file-button');
        presenter.views.fileSentWrapper = presenter.$view.find('.file-sender-sent-file');
        presenter.views.fileSentLabel = presenter.$view.find('.file-sender-sent-file-label');
        presenter.views.fileSentReset = presenter.$view.find('.file-sender-sent-file-reset');
    }

    presenter.getSentFileName = function() {
        return presenter.sentFileName;
    }

    presenter.getSentFileUrl = function() {
        return presenter.sentFileUrl;
    }

    presenter.isSupportedFileType = function(file) {
        var fileTypes = [".doc",".docx",".pdf",".odt",".jpg",".gif",".png",".html",".mp3",".wav"];
        var fileName = file.name.toLowerCase();
        for (var i = 0; i &lt; fileTypes.length; i++) {
            if (fileName.endsWith(fileTypes[i])) return true;
        }
        return false;
    }

    presenter.attachHandlers = function() {
        presenter.views.formInput.click(function(e){
            if (!presenter.contextLoaded) return;
            e.preventDefault();
            var tmpForm = $('&lt;form&gt;');
            var tmpInput = $('&lt;input type="file"&gt;');
            tmpInput.attr("accept", ".doc,.docx,.pdf,.odt,.jpg,.gif,.png,.html,.mp3,.wav");
            tmpForm.append(tmpInput);
            tmpInput.change(function(){
                presenter.views.formInput[0].files = tmpInput[0].files;
                presenter.views.formInput.change();
            });
            tmpInput.click();
        });
        presenter.views.formInput.change(function(e){
            if (presenter.contextLoaded) {
                var files = presenter.views.formInput[0].files;
                if(files.length &gt; 0 &amp;&amp; presenter.isSupportedFileType(files[0])) {
                    presenter.sendFile();
                }
            } else {
                presenter.views.formInput[0].value = null;
            }
        });
        presenter.views.sendFileButton.click(presenter.onSendFileClick);
        presenter.views.fileSentLabel.click(function() {
            var element = document.createElement("a");
            element.setAttribute("id", "dl");
            element.setAttribute("download", presenter.getSentFileName());
            element.setAttribute("href", presenter.getSentFileUrl());
            element.click();
        });
        presenter.views.fileSentReset.click(function() {
            presenter.reset();
        });
    }

    presenter.updateFileLabel = function() {
        if (presenter.views.formInput.length == 0 || presenter.views.formInput[0].files.length == 0) {
            presenter.views.formLabel.html("");
        }
        presenter.views.formLabel.html(presenter.views.formInput[0].files[0].name);
    }

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.showTeacherList = function() {
        presenter.onSendFileClick();
    }

    presenter.onSendFileClick = function() {
        if (!presenter.contextLoaded) return;
        
        if (presenter.configuration.sourceType == SOURCE_TYPES.FILE) {
            if (presenter.sentFileId != -1) {
                if (!presenter.fileSenderPickRecipientAvailable) {
                    presenter.sendFileToAllTeachers();
                } else {
                    presenter.showTargetDialog();
                }
            }
        } else if (presenter.configuration.sourceType == SOURCE_TYPES.MEDIA_RECORDER) {
            var module = presenter.getMediaRecorderModule();
            if (module != null &amp;&amp; !module.isEmpty()) {
                if (!presenter.fileSenderPickRecipientAvailable) {
                    presenter.sendFileToAllTeachers();
                } else {
                    presenter.showTargetDialog();
                }
            }
        } else {
            if (presenter.getParagraphModule() != null) {
                if (!presenter.fileSenderPickRecipientAvailable) {
                    presenter.sendFileToAllTeachers();
                } else {
                    presenter.showTargetDialog();
                }
            }
        }
    }

    presenter.sendFileToAllTeachers = function() {
        presenter.teachers.forEach(teacher =&gt; {
            presenter.sendFile(teacher["id"]);
        });
    }

    presenter.setSentFile = function(fileName, fileId) {
        presenter.sentFileName = fileName;
        presenter.sentFileId = fileId;
        var fileUrl = presenter.fileDownloadEndpointUrl + fileId;
        presenter.sentFileUrl = fileUrl;
        presenter.views.fileSentLabel.html(fileName);
    }

    presenter.resetSentFile = function() {
        presenter.sentFileName = "";
        presenter.sentFileUrl = "";
        presenter.sentFileId = -1;
        presenter.views.fileSentLabel.html("");
        presenter.isMessageSent = false;
    }

    presenter.reset = function() {
        presenter.resetSentFile();
        presenter.hideSentFile();
        presenter.showSendButton();
    }

    presenter.showSentFile = function() {
        presenter.views.fileSentWrapper.css("display", "");
        if (presenter.configuration.sourceType === SOURCE_TYPES.FILE) {
            presenter.views.form.css("display", "none");
        }
    }

    presenter.hideSentFile = function() {
        presenter.views.fileSentWrapper.css("display", "none");
        if (presenter.configuration.sourceType === SOURCE_TYPES.FILE) {
            presenter.views.form.css("display", "");
        }
    }

    presenter.hideSendButton = function() {
        presenter.views.sendFileButton.css("display", "none");
    }

    presenter.showSendButton = function() {
        if (!presenter.configuration.disableSendButton) {
            presenter.views.sendFileButton.css("display", "block");
        }
    }

    presenter.fetchSessionJWTToken = function() {
        return fetch('/api/v2/jwt/session_token', {
            method: 'GET'
        });
    }

    presenter.showTargetDialog = function() {
        //if (presenter.teachers.length == 0) return;
        var wrapper = $('&lt;div&gt;&lt;/div&gt;');
        wrapper.addClass('file-sender-teacher-dialog-content');
        for (var i = 0; i &lt; presenter.teachers.length; i++) {
            var teacher = presenter.teachers[i];
            var teacherElement = $('&lt;div&gt;&lt;/div&gt;');
            teacherElement.addClass('file-sender-teacher-dialog-element');
            teacherElement.html(presenter.getTeacherName(teacher));
            teacherElement.attr('data-targetId', teacher.id);
            teacherElement.click(presenter.onTeacherSelect);
            wrapper.append(teacherElement);
        }
        currentScrollTop = presenter.playerController.iframeScroll();
        presenter.findICPage();

        presenter.dialog = wrapper.dialog({
                    modal: true,
                    autoOpen: false,
                    title: presenter.configuration.dialogTitle,
                    open: presenter.openDialogEventHandler,
                    close: presenter.closeDialogEventHandler
                    });
        presenter.dialog.dialog('open');
        var dialogParent = presenter.dialog.closest('.ui-dialog');
        dialogParent.addClass('file-sender-teacher-dialog');
        dialogParent.find('.ui-dialog-titlebar').addClass('file-sender-teacher-dialog-titlebar');

        if (!presenter.playerController.isPlayerInCrossDomain()) {
            $(top.window).scrollTop(currentScrollTop);
        }
    }

    presenter.showErrorDialog = function(message) {
        //if (presenter.teachers.length == 0) return;
        var wrapper = $('&lt;div&gt;&lt;/div&gt;');
        wrapper.addClass('file-sender-error-dialog-content');
        wrapper.html(message);

        currentScrollTop = presenter.playerController.iframeScroll();
        presenter.findICPage();

        var dialog = wrapper.dialog({
                    modal: true,
                    autoOpen: false,
                    title: "Warning",
                    open: presenter.openDialogEventHandler,
                    close: presenter.closeDialogEventHandler
                    });
        dialog.dialog('open');
        var dialogParent = dialog.closest('.ui-dialog');
        dialogParent.addClass('file-sender-error-dialog');
        dialogParent.find('.ui-dialog-titlebar').addClass('file-sender-error-dialog-titlebar');

        if (!presenter.playerController.isPlayerInCrossDomain()) {
            $(top.window).scrollTop(currentScrollTop);
        }
    }

    presenter.getTeacherName = function(teacher) {
        if (teacher.first_name.length === 0 &amp;&amp; teacher.last_name.length === 0) {
            return teacher.username;
        }
        return (teacher.first_name + " " + teacher.last_name).trim();

    }

    presenter.onTeacherSelect = function(event) {
        presenter.dialog.dialog('close');
        var targetId = $(this).attr('data-targetId');
        if (targetId == null) return;
        if (presenter.configuration.sourceType == SOURCE_TYPES.FILE) {
            if (presenter.sentFileId != -1) {
                presenter.fireSendFileEvent(presenter.sentFileId, targetId);
                presenter.hideSendButton();
            }
        } else {
            presenter.sendFile(targetId);
        }
    }

    //targetID is optional,
    presenter.sendFile = function(targetID) {
        if (!presenter.contextLoaded) return;

        presenter.fetchSessionJWTToken().then(result =&gt; result.json()).then(json =&gt; {
            presenter.getFile().then(file =&gt; {
                if (file == null) {
                    return;
                }
                if (presenter.fileEndpointUrl.length == 0) {
                    return;
                }
                var formData = new FormData();
                formData.append('file', file, file.name);
                fetch(presenter.fileEndpointUrl, {
                    method: 'GET',
                    headers: {
                        'Authorization': 'JWT ' + json.token,
                    },
                    credentials: credentialsConfig
                }).then(result =&gt; result.json()).then(
                    success =&gt; fetch(success["upload_url"], {
                        method: 'POST',
                        headers: {
                            'Authorization': 'JWT ' + json.token
                        },
                        body: formData,
                        credentials: credentialsConfig
                    })
               ).then(result =&gt; result.json()).then(
                    success =&gt; {
                        if (success.uploaded_file_id === undefined) {
                            throw success;
                        }
                        presenter.setSentFile(file.name, success.uploaded_file_id);
                        presenter.showSentFile();
                        if (targetID !== undefined) {
                            presenter.fireSendFileEvent(success.uploaded_file_id, targetID);
                            presenter.hideSendButton();
                        }
                    }
                ).catch((err) =&gt; {
                        console.log(err);
                        presenter.showErrorDialog("Error occurred while uploading");
                    }
                );
            });
        });
    }

    presenter.executeCommand = function(name, params) {
        if (!presenter.configuration.isValid || !presenter.contextLoaded) return;

        var commands = {
            'showTeacherList': presenter.showTeacherList,
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getParagraphModule = function() {
        if (presenter.configuration.sourceType !== SOURCE_TYPES.PARAGRAPH) return null;
        var module = presenter.playerController.getModule(presenter.configuration.sourceId);
        if (module == null || !module.hasOwnProperty('getText')) return null;
        return module;
    }


    presenter.getTextFile = function() {
        var module = presenter.getParagraphModule();
        if (module == null) return null;
        var text = module.getText();
        var file = new File([text], "paragraph.html", {type: "text/html;charset=utf-8"});
        return file;
    }

    presenter.getMediaRecorderModule = function() {
        if (presenter.configuration.sourceType !== SOURCE_TYPES.MEDIA_RECORDER) return null;
        var module = presenter.playerController.getModule(presenter.configuration.sourceId);
        if (module == null || !module.hasOwnProperty('getMP3File') || !module.hasOwnProperty('isEmpty')) return null;
        return module;
    }

    presenter.getMP3File = function() {
        var module = presenter.getMediaRecorderModule();
        if (module == null) return null;
        return module.getMP3File();
    }

    presenter.getFile = function() {
        return new Promise(resolve =&gt; {
        if (presenter.configuration.sourceType === SOURCE_TYPES.PARAGRAPH) {
            resolve(presenter.getTextFile());
        } else if (presenter.configuration.sourceType === SOURCE_TYPES.FILE) {
            if (presenter.views.formInput[0].files.length &gt; 0) {
                 resolve(presenter.views.formInput[0].files[0]);
            } else {
                resolve(null);
            }
        } else { //SOURCE_TYPES.MEDIA_RECORDER
            var mp3 = presenter.getMP3File();
            if (mp3 == null) {
                resolve(null);
            } else {
                mp3.then(file =&gt; resolve(file));
            }
        }
        });
    }

    presenter.openDialogEventHandler = function(event, ui) {
        try{
            var $dialog  = $(event.target).closest('.ui-dialog');
            var isPreview = $(".gwt-DialogBox").is('.gwt-DialogBox');
            var isPopup =  $(presenter.$ICPage).is('.ic_popup_page');
            var isMarginalPage =  $(presenter.$ICPage).is('.ic_footer') || $(presenter.$ICPage).is('.ic_header');
            var presentationPosition = $(presenter.$ICPage).offset();
            var presentationWidth = $(presenter.$ICPage).outerWidth();
            var presentationHeight = isMarginalPage ?  $('.ic_page').outerHeight() : $(presenter.$ICPage).outerHeight();
            var dialogWidth = $dialog.outerWidth();
            var dialogHeight = $dialog.outerHeight();
            var topWindow = window;
            if (presenter.playerController &amp;&amp; !presenter.playerController.isPlayerInCrossDomain()) topWindow = top.window;
            var windowHeight = $(topWindow).height();
            var scrollTop = currentScrollTop;
            var previewFrame = 0;
            var popupTop = 0;
            var popupLeft = 0;
            var topPosition = 0;

            if (presenter.isPreview) {
                scrollTop = $(presenter.$ICPage).scrollTop();

                if (scrollTop &gt; 0) {
                    previewFrame = $(presenter.$ICPage).parent().parent().parent().offset().top - $(".gwt-DialogBox").offset().top;
                }

                windowHeight = ($(presenter.$ICPage).parent().parent().parent().height());
                presentationPosition.top = 0;
            }

            if (isPopup) {
                scrollTop = $(presenter.$ICPage).scrollTop();
                popupTop =  presentationPosition.top;
                if ($(topWindow).scrollTop() &gt; 0) presentationPosition.top = 0;
            }
            var visibleArea = presenter.estimateVisibleArea(presentationPosition.top, presentationHeight, scrollTop, windowHeight);
            var availableHeight = visibleArea.bottom - visibleArea.top;

            if (dialogHeight &gt;= availableHeight) {
                dialogHeight = presenter.calculateReducedDialogHeight($dialog, availableHeight);
                $dialog.find('.file-sender-teacher-dialog-content').css({
                    height: dialogHeight + 'px'
                });
            }

            // Check if the addon needs to account for transform css
            var scaleInfo = presenter.playerController.getScaleInformation();
            if(scaleInfo.scaleY!==1.0) {
                $dialog.css('transform', scaleInfo.transform);
                $dialog.css('transform-origin', scaleInfo.transformOrigin);
            }

            if(parseFloat(window.MobileUtils.getAndroidVersion())=='4.1'){
                if (window !== window.top) {
                    var ancestorData;
                    for (var i=0; i&lt;presenter.ancestorsData.length; i++)
                    {
                        ancestorData = presenter.ancestorsData[i];
                        $(ancestorData.wnd).scrollTop(ancestorData.offset);
                    }
                    presenter.ancestorsData = undefined;
                }
            }

            if (isPopup || presenter.isPreview) {
                popupLeft = presentationPosition.left;
                topPosition = parseInt((availableHeight - dialogHeight) / 2, 10);
            }
            else {
                topPosition = parseInt(( windowHeight - dialogHeight) / 2, 10) ;
            }

            var presentationHorizontalOffset = parseInt((presentationWidth - dialogWidth) * scaleInfo.scaleY / 2, 10);
            var leftPosition = presentationPosition.left + presentationHorizontalOffset;

            // adjust top position if Player was embedded in iframe (i.e. EverTeach)
            if (window !== window.top) {
                var iframeDialogHeight = parseInt($dialog.height(), 10);
                iframeDialogHeight += DOMOperationsUtils.calculateOuterDistances(DOMOperationsUtils.getOuterDimensions($dialog)).vertical;

                if (topPosition &lt; 0) {
                    topPosition = 0;
                } else if (topPosition &gt; $(window).height() - iframeDialogHeight) {
                    topPosition = $(window).height() - iframeDialogHeight;
                }
            }

            if (presenter.isPlayerInCrossDomain() &amp;&amp; !presenter.isMauthor()) {
                const _scrollTop = $(window.parent.document.getElementsByClassName('mdl-layout'))?.scrollTop();
                scrollTop = _scrollTop !== undefined ? _scrollTop : scrollTop;
            }

            if ($(window).scrollTop() &gt; popupTop &amp;&amp; isPopup) {
                topPosition += ($(window).scrollTop() - popupTop);
            }

            $dialog.css({
                left: (leftPosition - popupLeft) + 'px',
                top: (topPosition + scrollTop + previewFrame) + 'px'
            });

            $dialog.find('.ui-dialog-content').css({
                color: 'black'
            });

            if(isPopup || presenter.isPreview) {
                // For Preview and Popup dialog is moved to appropriate page
                var $overlay = $(".ui-widget-overlay");
                $(presenter.$view.closest(".ui-widget-overlay")).remove();
                if (isPreview) {
                    $(".ic_page_panel").children(".ic_page").children().last().after($overlay);
                }
                else {
                    $dialog.before($overlay);
                }
            }
        }catch(e){console.log(e)}
    };

    presenter.isPlayerInCrossDomain = function () {
        return presenter.playerController &amp;&amp; !presenter.playerController.isPlayerInCrossDomain();
    }

    presenter.isMauthor = function () {
        const names = ["lorepo", "mauthor"];
        const origin = window.origin;
        return names.some((name) =&gt; origin.includes(name));
    };

    presenter.closeDialogEventHandler = function() {
        // due to the inability to close the dialog, when any video is under close button
        try{
            presenter.dialog.css("maxHeight", "none");

            if (presenter.ancestorsData !== undefined) {
                var ancestorData;
                for (i=0; i&lt;presenter.ancestorsData.length; i++)
                {
                    ancestorData = presenter.ancestorsData[i];
                    $(ancestorData.wnd).scrollTop(ancestorData.offset);
                }
                presenter.ancestorsData = undefined;
            }
        }catch(e){}
    };

    presenter.findICPage = function () {
        presenter.$ICPage = $(presenter.$view.parent('.ic_page:first')[0]);
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_popup_page:first')[0]);
        }
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_header:first')[0]);
        }
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_footer:first')[0]);
        }
    };

    presenter.estimateVisibleArea = function(presentationTop, presentationHeight, scrollTop, windowHeight) {
        var borders = {
            top: presentationTop,
            bottom: presentationTop + presentationHeight
        };

        if (presentationTop &lt; scrollTop) {
            borders.top = scrollTop;
        }

        if (presentationTop + presentationHeight &gt; scrollTop + windowHeight) {
            borders.bottom = scrollTop + windowHeight;
        }

        return borders;
    };

    presenter.calculateReducedDialogHeight = function($dialog, pageHeight) {
        var titleHeight = $dialog.find(".ui-dialog-titlebar").outerHeight();
        var padding = parseInt($dialog.css("padding-top")) + parseInt($dialog.css("padding-bottom"));

        var $content = $dialog.find('.ui-dialog-content');
        var contentPadding = parseInt($content.css('paddingTop'), 10) + parseInt($content.css('paddingBottom'), 10);
        var contentBorder = parseInt($content.css('borderTopWidth'), 10) + parseInt($content.css('borderBottomWidth'), 10);
        var contentMargin = parseInt($content.css('marginTop'), 10) + parseInt($content.css('marginBottom'), 10);

        return pageHeight - padding - titleHeight - contentPadding - contentBorder - contentMargin - 100;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="FlashCards" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
		<property displayName="Cards" name="Cards" nameLabel="FlashCards_property_Cards" type="list">
            <property displayName="Front" name="Front" nameLabel="FlashCards_property_Front" type="html"/>
            <property displayName="Back" name="Back" nameLabel="FlashCards_property_Back" type="html"/>
			<property displayName="Audio Front" name="AudioFront" nameLabel="FlashCards_property_Audio_Front" type="audio"/>
			<property displayName="Audio Back" name="AudioBack" nameLabel="FlashCards_property_Audio_Back" type="audio"/>
        </property>
        <property displayName="Disable Loop" name="NoLoop" nameLabel="FlashCards_property_Disable_Loop" type="boolean"/>
        <property displayName="Enable Favourites" name="Favourites" nameLabel="FlashCards_property_Enable_Favourites" type="boolean"/>
        <property displayName="Hide Previous and Next Buttons" name="HidePrevNext" nameLabel="FlashCards_property_Hide_Previous_and_Next_Buttons" type="boolean"/>
        <property displayName="Show Activity Buttons" name="ShowButtons" nameLabel="FlashCards_property_Show_Activity_Buttons" type="boolean"/>
        <property displayName="Is Activity" name="IsActivity" nameLabel="FlashCards_property_Is_Activity" type="boolean"/>
        <property displayName="Send event only on card changed" name="sendEventOnCardChanged" nameLabel="FlashCards_property_eventOnCardChanged" type="boolean"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="FlashCards_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="FlashCards_property_speech_texts" type="staticlist">

            <property name="card" nameLabel="FlashCards_property_card" type="staticrow">
                <property name="card" nameLabel="FlashCards_property_card" type="string"/>
            </property>

            <property name="outOf" nameLabel="FlashCards_property_outOf" type="staticrow">
                <property name="outOf" nameLabel="FlashCards_property_outOf" type="string"/>
            </property>

            <property name="favourite" nameLabel="FlashCards_property_favourite" type="staticrow">
                <property name="favourite" nameLabel="FlashCards_property_favourite" type="string"/>
            </property>

            <property name="audio" nameLabel="FlashCards_property_audio" type="staticrow">
                <property name="audio" nameLabel="FlashCards_property_audio" type="string"/>
            </property>

            <property name="correct" nameLabel="FlashCards_property_correct" type="staticrow">
                <property name="correct" nameLabel="FlashCards_property_correct" type="string"/>
            </property>

            <property name="wrong" nameLabel="FlashCards_property_wrong" type="staticrow">
                <property name="wrong" nameLabel="FlashCards_property_wrong" type="string"/>
            </property>

            <property name="reset" nameLabel="FlashCards_property_reset" type="staticrow">
                <property name="reset" nameLabel="FlashCards_property_reset" type="string"/>
            </property>

            <property name="selected" nameLabel="FlashCards_property_selected" type="staticrow">
                <property name="selected" nameLabel="FlashCards_property_selected" type="string"/>
            </property>

            <property name="deselected" nameLabel="FlashCards_property_deselected" type="staticrow">
                <property name="deselected" nameLabel="FlashCards_property_deselected" type="string"/>
            </property>

            <property name="cardHasBeenReset" nameLabel="FlashCards_property_cardHasBeenReset" type="staticrow">
                <property name="cardHasBeenReset" nameLabel="FlashCards_property_cardHasBeenReset" type="string"/>
            </property>

            <property name="turned" nameLabel="FlashCards_property_turned" type="staticrow">
                <property name="turned" nameLabel="FlashCards_property_turned" type="string"/>
            </property>

        </property>
        <property displayName="Randomize order" name="randomize" nameLabel="FlashCards_property_randomize" type="boolean"/>
    </model>
<css>.flashcards-wrapper {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
}

.flashcards-main{
    display: flex;
    width: 100%;
    height: 100%;
    perspective: 1000px;
}

.flashcards-prev, .flashcards-next {
    display: block;
    width: 30px;
    height: 100%;
    background: #e8e8e8;
    cursor: pointer;
    border: none;
}
.flashcards-prev:disabled, .flashcards-next:disabled {
    opacity: 0.5;
    cursor: default;
}

.flashcards-card {
    position: relative;
    width: 100%;
    height: 100%;
}
.flashcards-card-audio-wrapper{
    display: none;
    position: absolute;
    width: 40px;
    height: 40px;
    top: 40px;
    left: 10px;
}
    .flashcards-card-audio-button{
        cursor: pointer;
        width: 40px;
        height: 40px;
        border-radius: 50%;
        background-color: #017aaf;
        background-image: url(/file/serve/5957429735063552);
        background-size: 30px 30px;
        background-repeat: no-repeat;
        background-position: center center;
    }
    .flashcards-card-audio-button.playing{
        background-image: url(/file/serve/6424961084424192);
    }
    .flashcards-card-audio-button.disabled{
        cursor: pointer;
        opacity: 0.5;    
    }

.flashcards-card.flashcards-card-reversed .flashcards-card-front{
    transform: rotateY(-180deg);
}
.flashcards-card.flashcards-card-reversed .flashcards-card-back {
    transform: rotateY(0deg);
}

.flashcards-card-front, .flashcards-card-back {
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    bottom: 0;
    backface-visibility: hidden;
    transition: transform 0.5s;
    transform-style: preserve-3d;
    -webkit-backface-visibility: hidden;
}
.flashcards-card-front {
    background: #eee;
    color: black;
}
.flashcards-card-back {
    background: #d3e3f3;
    color: #000;
    transform: rotateY(180deg);
}
    .flashcards-card-contents {
        padding: 10px;
        top: 0;
        left: 0;
        right: 0;
        bottom: 50px;
        position: absolute;
        overflow-y: auto;
        cursor: pointer;
    }
    .flashcards-card-contents img {
        max-width: 100%;
    }

.flashcards-button-favourite{
    position: absolute;
    width: 30px;
    height: 30px;
    top:0;
    right: 0;
    background: #999;
    cursor: pointer;
}
.flashcards-button-favourite.flashcards-button-selected {
    background: #83e1f7;
}

.flashcards-buttons{
    position: absolute;
    bottom: 0px;
    width: 100%;
    height: 50px;
    background: #ddd;
    display: flex;
    cursor: pointer;
}
    .flashcards-button {
        width: 33.3333%;
        text-align: center;
        line-height: 50px;
        color: #000;
    }
    .flashcards-button.flashcards-button-selected {
        background: #83e1f7;
    }

.flashcards-panel{
    height: 30px;
    width: 100%;
    background: #e8e8e8;
    text-align: center;
    color: #000;
    line-height: 30px;
}
</css><view>&lt;div class="flashcards-wrapper"&gt;
    &lt;div class="flashcards-main"&gt;
        &lt;div class="flashcards-prev-wrapper"&gt;
            &lt;button type="button" class="flashcards-prev"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="flashcards-card"&gt;
            &lt;div class="flashcards-card-front"&gt;
                &lt;div class="flashcards-card-contents flashcards-card-contents-front"&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-card-audio-wrapper flashcards-card-audio-wrapper-front"&gt;
                    &lt;audio class="flashcards-card-audio flashcards-card-audio-front"&gt;&lt;/audio&gt;
                    &lt;div class="flashcards-card-audio-button flashcards-card-audio-button-front disabled"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="flashcards-card-back"&gt;
                &lt;div class="flashcards-card-contents flashcards-card-contents-back"&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-buttons"&gt;
                    &lt;div class="flashcards-button flashcards-button-wrong"&gt;
                        x
                    &lt;/div&gt;
                    &lt;div class="flashcards-button flashcards-button-reset"&gt;
                        o
                    &lt;/div&gt;
                    &lt;div class="flashcards-button flashcards-button-correct"&gt;
                        v
                    &lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-card-audio-wrapper flashcards-card-audio-wrapper-back"&gt;
                    &lt;audio class="flashcards-card-audio flashcards-card-audio-back"&gt;&lt;/audio&gt;
                    &lt;div class="flashcards-card-audio-button flashcards-card-audio-button-back disabled"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="flashcards-button-favourite"&gt;&lt;/div&gt;
            &lt;div class="flashcards-card-audio-wrapper flashcards-card-audio-wrapper-hidden"&gt;
                &lt;audio class="flashcards-card-audio flashcards-card-audio-hidden"&gt;&lt;/audio&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="flashcards-prev-wrapper"&gt;
            &lt;button type="button" class="flashcards-next"&gt;&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="flashcards-panel"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="flashcards-wrapper"&gt;
    &lt;div class="flashcards-main"&gt;
        &lt;div class="flashcards-prev-wrapper"&gt;
            &lt;button type="button" class="flashcards-prev"&gt;&lt;/button&gt;
        &lt;/div&gt;
        &lt;div class="flashcards-card"&gt;
            &lt;div class="flashcards-card-front"&gt;
                &lt;div class="flashcards-card-contents flashcards-card-contents-front"&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-card-audio-wrapper flashcards-card-audio-wrapper-front"&gt;
                    &lt;audio class="flashcards-card-audio flashcards-card-audio-front"&gt;&lt;/audio&gt;
                    &lt;div class="flashcards-card-audio-button flashcards-card-audio-button-front disabled"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="flashcards-card-back"&gt;
                &lt;div class="flashcards-card-contents flashcards-card-contents-back"&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-buttons"&gt;
                    &lt;div class="flashcards-button flashcards-button-wrong"&gt;
                        x
                    &lt;/div&gt;
                    &lt;div class="flashcards-button flashcards-button-reset"&gt;
                        o
                    &lt;/div&gt;
                    &lt;div class="flashcards-button flashcards-button-correct"&gt;
                        v
                    &lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="flashcards-card-audio-wrapper flashcards-card-audio-wrapper-back"&gt;
                    &lt;audio class="flashcards-card-audio flashcards-card-audio-back"&gt;&lt;/audio&gt;
                    &lt;div class="flashcards-card-audio-button flashcards-card-audio-button-back disabled"&gt;&lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            &lt;div class="flashcards-button-favourite"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="flashcards-prev-wrapper"&gt;
            &lt;button type="button" class="flashcards-next"&gt;&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="flashcards-panel"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonFlashCards_create(){

    var presenter = function () {}

    presenter.configuration = {
        isVisible: false,
        currentCard: 1,
        noLoop: false,
        IsActivity: false,
        Favourites: false
    };

    presenter.state = {
        isVisible: false,
        currentCard: 1,
        totalCards: 1,
        noLoop: false,
        IsActivity: false,
        Favourites: false,
        ShowOnlyFavourites: false,
        cardsScore: null,
        cardsFavourites: null
    };

    presenter.cardMap = [];

    presenter.isLoaded = false;
    presenter.isPreview = false;
    presenter.textParser = null;

    let isWCAGOn = false;
    presenter.speechTexts = {};
    let KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS = "keyboard_navigation_active_element";

    let DEFAULT_TTS_PHRASES = {
        card: "Card",
        outOf: "out of",
        favourite: "Favourite",
        audio: "Audio",
        correct: "Correct",
        wrong: "Wrong",
        reset: "Reset",
        selected: "Selected",
        deselected: "Deselected",
        cardHasBeenReset: "Card has been reset",
        turned: "Turned"
    };

    presenter.lastHiddenAudio = {
        cardIndex: -1,
        reverse: false
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);

        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.createEventData = function (item, value, score) {
        return {
            source : presenter.configuration.addonID,
            item: item,
            value: value,
            score: score
        };
    };

    presenter.triggerEvent = function (item, value, score) {
        var eventData = presenter.createEventData(item, value, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.validateModel = function (model) {
        presenter.setSpeechTexts(model['speechTexts']);
        return {
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            noLoop: ModelValidationUtils.validateBoolean(model['NoLoop']),
            Favourites: ModelValidationUtils.validateBoolean(model['Favourites']),
            HidePrevNext: ModelValidationUtils.validateBoolean(model['HidePrevNext']),
            ShowButtons: ModelValidationUtils.validateBoolean(model['ShowButtons']),
            IsActivity: ModelValidationUtils.validateBoolean(model["IsActivity"]),
            currentCard: presenter.configuration.currentCard,
            cardsScore: [],
            cardsFavourites: [],
            addonID: model['ID'],
            langTag: model['langAttribute'],
            randomizeOrder: ModelValidationUtils.validateBoolean(model['randomize']),
            sendEventOnCardChanged: ModelValidationUtils.validateBoolean(model["sendEventOnCardChanged"])
        };
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeAddTTS(model);
        upgradedModel = presenter.upgradeSendEventOnCardChanged(upgradedModel);

        return presenter.upgradeAddRandomize(upgradedModel);
    };

    presenter.upgradeAddTTS = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
            upgradedModel["speechTexts"]["card"] = {card: ""};
            upgradedModel["speechTexts"]["outOf"] = {outOf: ""};
            upgradedModel["speechTexts"]["favourite"] = {favourite: ""};
            upgradedModel["speechTexts"]["audio"] = {audio: ""};
            upgradedModel["speechTexts"]["correct"] = {correct: ""};
            upgradedModel["speechTexts"]["wrong"] = {wrong: ""};
            upgradedModel["speechTexts"]["reset"] = {reset: ""};
            upgradedModel["speechTexts"]["selected"] = {selected: ""};
            upgradedModel["speechTexts"]["deselected"] = {deselected: ""};
            upgradedModel["speechTexts"]["cardHasBeenReset"] = {cardHasBeenReset: ""};
            upgradedModel["speechTexts"]["turned"] = {turned: ""};
        }
        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = ""
        }
        return upgradedModel;
    };

    presenter.upgradeSendEventOnCardChanged = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('sendEventOnCardChanged')) {
            upgradedModel['sendEventOnCardChanged'] = 'False';
        }

        return upgradedModel;
    };

    presenter.upgradeAddRandomize = function (model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["randomize"]) {
                upgradedModel["randomize"] = "False"
            }
            return upgradedModel;
        };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            card: DEFAULT_TTS_PHRASES.card,
            outOf: DEFAULT_TTS_PHRASES.outOf,
            favourite: DEFAULT_TTS_PHRASES.favourite,
            audio: DEFAULT_TTS_PHRASES.audio,
            correct: DEFAULT_TTS_PHRASES.correct,
            wrong: DEFAULT_TTS_PHRASES.wrong,
            reset: DEFAULT_TTS_PHRASES.reset,
            selected: DEFAULT_TTS_PHRASES.selected,
            deselected: DEFAULT_TTS_PHRASES.deselected,
            cardHasBeenReset: DEFAULT_TTS_PHRASES.cardHasBeenReset,
            turned: DEFAULT_TTS_PHRASES.turned
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }
        presenter.speechTexts = {
            card: window.TTSUtils.getSpeechTextProperty(
                speechTexts.card.card,
                presenter.speechTexts.card),
            outOf: window.TTSUtils.getSpeechTextProperty(
                speechTexts.outOf.outOf,
                presenter.speechTexts.outOf),
            favourite: window.TTSUtils.getSpeechTextProperty(
                speechTexts.favourite.favourite,
                presenter.speechTexts.favourite),
            audio: window.TTSUtils.getSpeechTextProperty(
                speechTexts.audio.audio,
                presenter.speechTexts.audio),
            correct: window.TTSUtils.getSpeechTextProperty(
                speechTexts.correct.correct,
                presenter.speechTexts.correct),
            wrong: window.TTSUtils.getSpeechTextProperty(
                speechTexts.wrong.wrong,
                presenter.speechTexts.wrong),
            reset: window.TTSUtils.getSpeechTextProperty(
                speechTexts.reset.reset,
                presenter.speechTexts.reset),
            selected: window.TTSUtils.getSpeechTextProperty(
                speechTexts.selected.selected,
                presenter.speechTexts.selected),
            deselected: window.TTSUtils.getSpeechTextProperty(
                speechTexts.deselected.deselected,
                presenter.speechTexts.deselected),
            cardHasBeenReset: window.TTSUtils.getSpeechTextProperty(
                speechTexts.cardHasBeenReset.cardHasBeenReset,
                presenter.speechTexts.cardHasBeenReset),
            turned: window.TTSUtils.getSpeechTextProperty(
                speechTexts.turned.turned,
                presenter.speechTexts.turned),
        };
    };

    presenter.init = function (view, model, isPreview) {
        model = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(model);
        presenter.configuration = validatedModel;
        presenter.isErrorMode = false;
        presenter.isPreview = isPreview;
        presenter.Cards = model.Cards;
        presenter.generateCardMap();
        presenter.state.isVisible = presenter.configuration.isVisible;
        presenter.state.noLoop = presenter.configuration.noLoop;
        presenter.state.cardsScore = presenter.configuration.cardsScore;
        presenter.state.cardsFavourites = presenter.configuration.cardsFavourites;

        presenter.view = view;
        presenter.$view = $(view);
        presenter.flashcardsMain = $(presenter.$view.find(".flashcards-card").get(0));
        presenter.flashcardsPrev = presenter.$view.find(".flashcards-prev");
        presenter.flashcardsNext = presenter.$view.find(".flashcards-next");
        presenter.flashcardsButtonFavourite = presenter.$view.find(".flashcards-button-favourite");
        presenter.flashcardsButtonWrong = presenter.$view.find(".flashcards-button-wrong");
        presenter.flashcardsButtonCorrect = presenter.$view.find(".flashcards-button-correct");
        presenter.flashcardsButtonReset = presenter.$view.find(".flashcards-button-reset");
        presenter.flashcardsButton = presenter.$view.find(".flashcards-button");
        presenter.flashcardsCardAudioButtonFront = presenter.$view.find(".flashcards-card-audio-button-front");
        presenter.flashcardsCardAudioButtonBack = presenter.$view.find(".flashcards-card-audio-button-back");
        presenter.audioElementBack =  presenter.$view.find(".flashcards-card-audio-back").get(0);
        presenter.audioElementFront =  presenter.$view.find(".flashcards-card-audio-front").get(0);
        presenter.audioElementHidden =  presenter.$view.find(".flashcards-card-audio-hidden").get(0);
        
        presenter.model = model;
        presenter.$card = $(presenter.$view.find(".flashcards-card").get(0));

        $(presenter.Cards).each(function (key) {
            presenter.state.cardsScore[key] = 0;
            presenter.state.cardsFavourites[key] = false;
        });

        presenter.state.totalCards = presenter.Cards.length;

        if (presenter.configuration.HidePrevNext) {
            $(presenter.flashcardsPrev.get(0)).hide();
            $(presenter.flashcardsNext.get(0)).hide();
        }
        if (!presenter.configuration.ShowButtons) {
            $(presenter.$view.find(".flashcards-buttons").get(0)).hide();
        }
        if (presenter.configuration.Favourites == false) {
            $(presenter.flashcardsButtonFavourite.get(0)).hide();
        }        

        presenter.showCard(1);
        setTimeout(()=&gt; {
            presenter.addClickHandlers();
        }, 0);

        //audio
        presenter.isFrontPlaying = false;
        presenter.isBackPlaying = false;
        presenter.isHiddenPlaying = false;
        if (!presenter.isPreview) {
            presenter.addAudioEventHandlers();
            MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }

        presenter.isLoaded = true;
    };

    presenter.generateCardMap = function() {
        presenter.cardMap = [];
        for (var i = 0; i &lt; presenter.Cards.length; i++) presenter.cardMap.push(i);
        if (presenter.configuration.randomizeOrder) {
            for (var i = presenter.cardMap.length - 1; i &gt; 0; i--) {
                var j = Math.floor(Math.random() * (i + 1));
                var tmp = presenter.cardMap[i];
                presenter.cardMap[i] = presenter.cardMap[j];
                presenter.cardMap[j] = tmp;
            }
        }
    };

    presenter.getCard = function(index) {
        return presenter.Cards[presenter.cardMap[index]];
    }

    presenter.countFavourites = function () {
        var i = 0;
        $(presenter.Cards).each(function (k,v) {
            if (presenter.state.cardsFavourites[k] == true){i++};
        });
        return i;
    };

    presenter.countNonFavouritesBefore = function (k) {
        var i = 0;
        for(j = 0; j &lt; k; j++){
            if (presenter.state.cardsFavourites[presenter.cardMap[j]] == false){i++};
        }
        return i;
    };

    presenter.addClickHandlers = function () {
        //FLIP, PREV &amp; NEXT
        $(presenter.$view.find(".flashcards-card-contents")).click(function (e) {
            if (presenter.isErrorMode) return;
            e.preventDefault();
            presenter.revertCard();
        });
        $(presenter.flashcardsPrev).click(function (e) {
            if (presenter.isErrorMode) return;
            e.preventDefault();
            presenter.prevCard();
        });
        $(presenter.flashcardsNext).click(function (e) {
            if (presenter.isErrorMode) return;
            e.preventDefault();
            presenter.nextCard();
        });

        //SCORE BUTTONS
        $(presenter.flashcardsButtonWrong).click(function () {
            if (presenter.isErrorMode) return;
            presenter.state.cardsScore[presenter.cardMap[presenter.state.currentCard-1]+1] = -1;
            $(presenter.flashcardsButton).removeClass("flashcards-button-selected");
            $(this).addClass("flashcards-button-selected");
        });
        $(presenter.flashcardsButtonCorrect).click(function () {
            if (presenter.isErrorMode) return;
            presenter.state.cardsScore[presenter.cardMap[presenter.state.currentCard-1]+1] = 1;
            $(presenter.flashcardsButton).removeClass("flashcards-button-selected");
            $(this).addClass("flashcards-button-selected");
        });
        $(presenter.flashcardsButtonReset).click(function () {
            if (presenter.isErrorMode) return;
            presenter.state.cardsScore[presenter.cardMap[presenter.state.currentCard-1]+1] = 0;
            $(presenter.flashcardsButton).removeClass("flashcards-button-selected");
        });

        //FAVOURITE BUTTON
        $(presenter.flashcardsButtonFavourite).click(function () {
            if (presenter.isErrorMode) return;
            if (presenter.state.cardsFavourites[presenter.cardMap[presenter.state.currentCard - 1]] == false){
                presenter.state.cardsFavourites[presenter.cardMap[presenter.state.currentCard - 1]] = true;
                $(this).addClass("flashcards-button-selected");
                presenter.triggerEvent(presenter.state.currentCard,"favourite","");
            }else{
                presenter.state.cardsFavourites[presenter.cardMap[presenter.state.currentCard - 1]] = false;
                $(this).removeClass("flashcards-button-selected");
                presenter.triggerEvent(presenter.state.currentCard,"unfavourite","");
            }            
        });

        //AUDIO
        $(presenter.flashcardsCardAudioButtonFront).click(function () {
            if (presenter.isErrorMode) return;
            
            if (presenter.isFrontPlaying == false) {
                presenter.isFrontPlaying = true;
                presenter.resetHiddenAudio();
                presenter.audioElementFront.play();
                $(presenter.flashcardsCardAudioButtonFront).addClass("playing");
                $(presenter.flashcardsCardAudioButtonFront).removeClass("paused");
                presenter.sendPlayEvent();
            }else{
                presenter.isFrontPlaying = false;
                presenter.audioElementFront.pause();
                $(presenter.flashcardsCardAudioButtonFront).removeClass("playing");
                $(presenter.flashcardsCardAudioButtonFront).addClass("paused");
                presenter.sendPauseEvent();
            }
        });
        $(presenter.flashcardsCardAudioButtonBack).click(function () {
            if (presenter.isErrorMode) return;
            presenter.audioElementBack =  presenter.$view.find(".flashcards-card-audio-back").get(0);
            if (presenter.isBackPlaying == false) {
                presenter.isBackPlaying = true;
                presenter.resetHiddenAudio();
                presenter.audioElementBack.play();
                $(presenter.flashcardsCardAudioButtonBack).addClass("playing");
                $(presenter.flashcardsCardAudioButtonBack).removeClass("paused");
                presenter.sendPlayEvent();
            }else{
                presenter.isBackPlaying = false;
                presenter.audioElementBack.pause();
                $(presenter.flashcardsCardAudioButtonBack).removeClass("playing");
                $(presenter.flashcardsCardAudioButtonBack).addClass("paused");
                presenter.sendPauseEvent();
            }
        });  
    };

     presenter.addAudioEventHandlers = function () {
         presenter.audioElementFront.onended = function () {
             $(presenter.flashcardsCardAudioButtonFront).removeClass("playing");
             $(presenter.flashcardsCardAudioButtonFront).removeClass("paused");
             presenter.isFrontPlaying = false;
             presenter.sendEndedEvent();
         };

         presenter.audioElementBack.onended = function () {
             $(presenter.flashcardsCardAudioButtonBack).removeClass("playing");
             $(presenter.flashcardsCardAudioButtonBack).removeClass("paused");
             presenter.isBackPlaying = false;
             presenter.sendEndedEvent();
         };

        presenter.audioElementHidden.onended = function () {
            presenter.isHiddenPlaying = false;
            presenter.sendEndedEvent(presenter.lastHiddenAudio.cardIndex + 1);
        };
     };
    
    presenter.run = function (view, model) {
        presenter.init(view, model, false);
    };
    
    presenter.createPreview = function (view, model) {
        presenter.init(view, model, true);
    };

    presenter.revertCard = function () {
        if (presenter.isFrontPlaying || presenter.isBackPlaying) {
            presenter.sendPauseEvent();
            if (presenter.isFrontPlaying) {
                $(presenter.flashcardsCardAudioButtonFront).addClass("paused");
            } else {
                $(presenter.flashcardsCardAudioButtonBack).addClass("paused");
            }
        }
        presenter.isFrontPlaying = false;
        presenter.audioElementFront.pause();
        $(presenter.flashcardsCardAudioButtonFront).removeClass("playing");

        presenter.isBackPlaying = false;
        presenter.audioElementBack.pause();
        $(presenter.flashcardsCardAudioButtonBack).removeClass("playing");

        presenter.$card.find(".flashcards-card-back .flashcards-card-contents").css("visibility","visible");
        var $cardElement = $(presenter.$view.find(".flashcards-card").get(0));
        $cardElement.toggleClass("flashcards-card-reversed");
        if ($cardElement.is(".flashcards-card-reversed")) {
            presenter.sendCardReversedEvent(presenter.state.currentCard, true);
        } else {
            presenter.sendCardReversedEvent(presenter.state.currentCard, false);
        }

    };

    presenter.isFrontVisible = function() {
        return !$(presenter.$view.find(".flashcards-card").get(0)).hasClass("flashcards-card-reversed");
    }

    presenter.originalCard = 1;
    presenter.prevCard = function () {
        if ((presenter.isFrontPlaying || presenter.isBackPlaying) &amp;&amp; presenter.state.currentCard &gt; 1) {
            presenter.sendEndedEvent(presenter.originalCard);
        }
        presenter.removeActiveElementClass();
        if (presenter.state.currentCard&gt;1){
            presenter.state.currentCard -= 1;
        }else if (presenter.state.noLoop == false){
            presenter.state.currentCard = presenter.state.totalCards;
        }
        presenter.showCard(presenter.state.currentCard, true);
    };

    presenter.nextCard = function (disregardNoLoop) {
        presenter.removeActiveElementClass();
        if (presenter.state.currentCard &lt; presenter.state.totalCards){
            presenter.state.currentCard += 1;
        }else if (presenter.state.noLoop == false || disregardNoLoop){
            presenter.state.currentCard = 1;
        }
        if ((presenter.isFrontPlaying || presenter.isBackPlaying) &amp;&amp; presenter.state.currentCard != presenter.originalCard) {
            presenter.sendEndedEvent(presenter.originalCard);
        }
        presenter.showCard(presenter.state.currentCard, true);
    };

    presenter.showCard = function (cardNumber, sendEvent) {
        if (sendEvent == undefined) sendEvent = false;
        presenter.originalCard = presenter.state.currentCard;
        cardNumber = parseInt(cardNumber,10);
        if (presenter.state.ShowOnlyFavourites == true &amp;&amp; presenter.countFavourites() &gt; 0 ){
            if (presenter.state.cardsFavourites[presenter.cardMap[presenter.state.currentCard - 1]] == true){
                presenter.displayCard(cardNumber, sendEvent);
            }else{
                if (cardNumber &lt;= presenter.state.totalCards){
                    presenter.nextCard(true);
                }
            }
        }else{
            presenter.displayCard(cardNumber, sendEvent);
        }        
    };

    presenter.displayCard = function (cardNumber, sendEvent) {
        if (presenter.state.noLoop){
            $(presenter.flashcardsPrev.get(0)).attr("disabled", false);
            $(presenter.flashcardsNext.get(0)).attr("disabled", false);
            if (cardNumber == 1){
                $(presenter.flashcardsPrev.get(0)).attr("disabled", true);
            }else if (cardNumber == presenter.state.totalCards){
                $(presenter.flashcardsNext.get(0)).attr("disabled", true);
            }
        }

        var currentCardNumber = cardNumber;
        var totalCardsNuber = 0;
        if (presenter.state.ShowOnlyFavourites == true) {
            currentCardNumber = cardNumber - presenter.countNonFavouritesBefore(cardNumber);
            totalCardsNuber = presenter.countFavourites();
        }else{
            totalCardsNuber = presenter.state.totalCards;
        }
        presenter.$view.find(".flashcards-panel").get(0).innerHTML = currentCardNumber + "/" + totalCardsNuber;
        presenter.$card.find(".flashcards-card-back .flashcards-card-contents").css('visibility', 'hidden');
        presenter.$card.removeClass("flashcards-card-reversed");

        presenter.$view.find(".flashcards-card-contents-front").get(0).innerHTML = parseAltText(presenter.getCard(cardNumber - 1).Front);
        presenter.$view.find(".flashcards-card-contents-back").get(0).innerHTML = parseAltText(presenter.getCard(cardNumber - 1).Back);

        //SCORE BUTTONS
        $(presenter.flashcardsButton).removeClass("flashcards-button-selected");
        if (presenter.state.cardsScore[presenter.cardMap[presenter.state.currentCard-1]+1] == 1){
            $(presenter.flashcardsButtonCorrect).addClass("flashcards-button-selected");
        }
        if (presenter.state.cardsScore[presenter.cardMap[presenter.state.currentCard-1]+1] == -1){
            $(presenter.flashcardsButtonWrong).addClass("flashcards-button-selected");
        }

        //FAV BUTTON
        if (presenter.state.cardsFavourites[presenter.cardMap[presenter.state.currentCard - 1]] == true) {
            $(presenter.flashcardsButtonFavourite).addClass("flashcards-button-selected");
        }else{
            $(presenter.flashcardsButtonFavourite).removeClass("flashcards-button-selected");
        }

        //AUDIO - front
        presenter.isFrontPlaying = false;
        $(presenter.flashcardsCardAudioButtonFront).removeClass("playing");
        $(presenter.flashcardsCardAudioButtonFront).addClass("disabled");
        if (presenter.getCard(presenter.state.currentCard - 1).AudioFront != ""){
            $(presenter.$view.find(".flashcards-card-audio-wrapper-front")).show();
            if (presenter.audioElementFront.canPlayType("audio/mpeg")) {
                presenter.audioElementFront.setAttribute("src",presenter.getCard(presenter.state.currentCard - 1).AudioFront);
                presenter.audioElementFront.oncanplay = function () {
                    $(presenter.flashcardsCardAudioButtonFront).removeClass("disabled");
                };
            } 
        }else{
            presenter.audioElementFront.setAttribute("src","");
            $(presenter.$view.find(".flashcards-card-audio-wrapper-front")).hide();
        }
        //AUDIO - back
        presenter.isBackPlaying = false;
        $(presenter.flashcardsCardAudioButtonBack).removeClass("playing");
        $(presenter.flashcardsCardAudioButtonBack).addClass("disabled");
        if (presenter.getCard(presenter.state.currentCard - 1).AudioBack != ""){
            $(presenter.$view.find(".flashcards-card-audio-wrapper-back")).show();
            if (presenter.audioElementBack.canPlayType("audio/mpeg")) {
                presenter.audioElementBack.setAttribute("src",presenter.getCard(presenter.state.currentCard - 1).AudioBack);
                presenter.audioElementBack.oncanplay = function () {
                    $(presenter.flashcardsCardAudioButtonBack).removeClass("disabled");
                };
            } 
        }else{
            presenter.audioElementBack.setAttribute("src","");
            $(presenter.$view.find(".flashcards-card-audio-wrapper-back")).hide();
        }
        presenter.renderMathJax();
        if (sendEvent) {
            presenter.sendCardChangedEvent(cardNumber);
        }
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.renderMathJax();
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.$view.css("display", isVisible ? "" : "none");
    };

    presenter.updateVisibility = function () {
        (presenter.state.isVisible) ? presenter.show() : presenter.hide();
    };

    presenter.setShowErrorsMode = function () {
        presenter.isErrorMode = true;
    }; 

    presenter.setWorkMode = function () {
        presenter.isErrorMode = false;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function () {
        presenter.isErrorMode = true;
    };

    presenter.hideAnswers = function () {
        presenter.isErrorMode = false;
    };

    presenter.ShowOnlyFavourites = function () {presenter.showOnlyFavourites();};

    presenter.showOnlyFavourites = function () {
        if (presenter.countFavourites() &gt; 0) {
            presenter.state.ShowOnlyFavourites = true;
            presenter.showCard(presenter.state.currentCard);
        }
    };

    presenter.ShowAllCards = function () {presenter.showAllCards();};

    presenter.showAllCards = function () {
        presenter.state.ShowOnlyFavourites = false;
        presenter.showCard(presenter.state.currentCard);
    };

    presenter.resetFavourites = function () {
        presenter.state.ShowOnlyFavourites = false;
        $(presenter.Cards).each(function (key) {
            presenter.state.cardsFavourites[key] = false;
        });
        $(presenter.flashcardsButtonFavourite).removeClass("flashcards-button-selected");
    };
    
    presenter.reset = function () {
        presenter.isErrorMode = false;
        presenter.state.ShowOnlyFavourites = false;
        presenter.state.currentCard = presenter.configuration.currentCard;

        presenter.state.isVisible = presenter.configuration.isVisible;
        presenter.updateVisibility();

        $(presenter.model.Cards).each(function (key) {
            presenter.state.cardsScore[key] = 0;
            presenter.state.cardsFavourites[key] = false;
        });

        presenter.showCard(presenter.state.currentCard);
    };
    
    presenter.getErrorCount = function () {
        var errors = null;
        if (presenter.configuration.IsActivity) {
            errors = 0;
            $(presenter.state.cardsScore).each(function (key) {
                if (presenter.state.cardsScore[key] == -1){
                    errors++;
                }
            });
        }
        return errors;
    };
    
    presenter.getMaxScore = function () {
        var maxScore = null;
        if (presenter.configuration.IsActivity) {
            maxScore = presenter.state.totalCards;
        }
        return maxScore;
    };
    
    presenter.getScore = function () {
        var score = null;
        if (presenter.configuration.IsActivity) {
            score = 0;
            $(presenter.state.cardsScore).each(function (key) {
                if (presenter.state.cardsScore[key] == 1){
                    score++;
                }
            });
        }
        return score;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'nextCard': presenter.nextCard,
            'prevCard': presenter.prevCard,
            'reset': presenter.reset,
            'resetFavourites': presenter.resetFavourites,
            'ShowOnlyFavourites': presenter.showOnlyFavourites,
            'showOnlyFavourites': presenter.showOnlyFavourites,
            'ShowAllCards': presenter.showAllCards,
            'showAllCards': presenter.showAllCards,
            'countFavourites': presenter.countFavourites,
            'play': presenter.play,
            'pause': presenter.pause,
            'stop': presenter.stop
        };
        Commands.dispatch(commands, name, params, presenter);
    };
    
    presenter.getState = function () {
        return JSON.stringify({
            state: presenter.state
        });
    };

    presenter.setState = function (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;
        var parsedState = JSON.parse(stateString);
        presenter.state = parsedState.state;

        presenter.updateVisibility();
        if (presenter.configuration.randomizeOrder) {
            presenter.state.currentCard = 1;
        }
        presenter.showCard(presenter.state.currentCard);
    };

    presenter.destroy = function () {
        $(presenter.$view.find(".flashcards-card-contents")).unbind();
        $(presenter.flashcardsButtonWrong).unbind();
        $(presenter.flashcardsButtonCorrect).unbind();
        $(presenter.flashcardsButtonReset).unbind();
        $(presenter.flashcardsButtonFavourite).unbind();
        $(presenter.flashcardsButtonFavourite).unbind();
        $(presenter.flashcardsNext).unbind();
        
    };

    presenter.playHiddenCardAudio = function (cardIndex, reverse) {
        if (typeof reverse == 'string') {
            reverse = reverse.toLowerCase() == 'reverse' || reverse.toLowerCase() == 'true';
        }
        reverse = !!reverse;
        var card = presenter.Cards[cardIndex];
        if (!card) return;
        if (presenter.isFrontPlaying) {
            presenter.audioElementFront.pause();
            presenter.audioElementFront.currentTime = 0;
            presenter.isFrontPlaying = false;
            presenter.sendEndedEvent();
        }
        if (presenter.isBackPlaying) {
            presenter.audioElementBack.pause();
            presenter.audioElementBack.currentTime = 0;
            presenter.isBackPlaying = false;
            presenter.sendEndedEvent();
        }
        if ((!reverse &amp;&amp; card.AudioFront != "") || (reverse &amp;&amp; card.AudioBack != "")){
            if (presenter.audioElementHidden.canPlayType("audio/mpeg")) {
                var audioSrc = reverse ? card.AudioBack : card.AudioFront;
                if (presenter.lastHiddenAudio.cardIndex != cardIndex || presenter.lastHiddenAudio.reverse != reverse) {
                    presenter.lastHiddenAudio.cardIndex = cardIndex;
                    presenter.lastHiddenAudio.reverse = reverse;
                    presenter.audioElementHidden.setAttribute("src", audioSrc);
                    presenter.audioElementHidden.oncanplay = function () {
                        presenter.audioElementHidden.play();
                        presenter.sendPlayEvent(cardIndex + 1);
                        presenter.isHiddenPlaying = true;
                        presenter.audioElementHidden.oncanplay = null;
                    };
                } else if (!presenter.isHiddenPlaying) {
                    presenter.audioElementHidden.play();
                    presenter.sendPlayEvent(cardIndex + 1);
                    presenter.isHiddenPlaying = true;
                }
            }
        }
    }

    presenter.resetHiddenAudio = function() {
        if (presenter.isHiddenPlaying) {
            presenter.isHiddenPlaying = false;
            presenter.audioElementHidden.pause();
            presenter.sendEndedEvent(presenter.lastHiddenAudio.cardIndex + 1);
        }
        presenter.audioElementHidden.currentTime = 0;
    }

    presenter.sendEvent = function(eventName, cardNumber) {
        var item = presenter.state.currentCard;
        if (cardNumber !== undefined) item = cardNumber;
        item = presenter.cardMap[item - 1] + 1;
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': item,
            'value': eventName,
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.sendPlayEvent = function(cardNumber) {
        if (presenter.configuration.sendEventOnCardChanged) { return; }
        presenter.sendEvent("playing", cardNumber);};

    presenter.sendPauseEvent = function(cardNumber) {
        if (presenter.configuration.sendEventOnCardChanged) { return; }
        presenter.sendEvent("pause", cardNumber);};

    presenter.sendEndedEvent = function(cardNumber) {
        if (presenter.configuration.sendEventOnCardChanged) { return; }
        presenter.sendEvent("ended", cardNumber);
    };

    presenter.sendCardChangedEvent = function(cardNumber) {presenter.sendEvent("", cardNumber);};

    presenter.sendCardReversedEvent = function(cardNumber, isReversed) {
        if (presenter.configuration.sendEventOnCardChanged) { return; }
        var eventValue = isReversed ? "back" : "front";
        presenter.sendEvent(eventValue, cardNumber);
    };

    presenter.play = function(cardIndex = -1, reverse = false) {
        if (Array.isArray(cardIndex)) {
            if (cardIndex.length &gt; 0) {
                if (cardIndex.length &gt; 1) reverse = cardIndex[1];
                cardIndex = cardIndex[0];
            } else {
                cardIndex = -1;
            }
        }
        if (cardIndex != -1) {
            presenter.playHiddenCardAudio(cardIndex - 1, reverse);
        } else {
            presenter.audioCommand("play");
        }
    };

    presenter.pause = function() {
        presenter.audioCommand("pause");
    };

    presenter.stop = function() {
        presenter.audioCommand("stop");
    };

    presenter.audioCommand = function(command) {
        if (presenter.isErrorMode) return;
        if (presenter.isHiddenPlaying &amp;&amp; command != "play") {
            if (command == "stop" &amp;&amp; presenter.audioElementHidden.currentTime &gt; 0) {
                presenter.audioElementHidden.currentTime = 0;
                presenter.sendEndedEvent(presenter.lastHiddenAudio.cardIndex + 1);
            }
            presenter.audioElementHidden.pause();
            if (command == "pause") {
                presenter.sendPauseEvent(presenter.lastHiddenAudio.cardIndex + 1);
            }
            presenter.isHiddenPlaying = false;
            return;
        }

        var currentCard = presenter.getCard(presenter.state.currentCard - 1);
        if (presenter.isFrontVisible()) {
            if (currentCard.AudioFront != "") {
                if (command == "play") {
                    if (presenter.isFrontPlaying) return;
                    presenter.isFrontPlaying = true;
                    presenter.resetHiddenAudio();
                    presenter.audioElementFront.play();
                    $(presenter.flashcardsCardAudioButtonFront).addClass("playing");
                    $(presenter.flashcardsCardAudioButtonFront).removeClass("paused");
                    presenter.sendPlayEvent();
                }else{
                    if (command == "stop" &amp;&amp; presenter.audioElementFront.currentTime &gt; 0) {
                        presenter.audioElementFront.currentTime = 0;
                        presenter.sendEndedEvent();
                        $(presenter.flashcardsCardAudioButtonFront).removeClass("paused");
                    }
                    if (!presenter.isFrontPlaying) return;
                    presenter.isFrontPlaying = false;
                    presenter.audioElementFront.pause();
                    $(presenter.flashcardsCardAudioButtonFront).removeClass("playing");
                    if (command == "pause") {
                        $(presenter.flashcardsCardAudioButtonFront).addClass("paused");
                        presenter.sendPauseEvent();
                    }
                }
            }
        } else {
            if (currentCard.AudioBack != "") {
                presenter.audioElementBack =  presenter.$view.find(".flashcards-card-audio-back").get(0);
                if (command == "play") {
                    if (presenter.isBackPlaying == true) return;
                    presenter.isBackPlaying = true;
                    presenter.resetHiddenAudio();
                    presenter.audioElementBack.play();
                    $(presenter.flashcardsCardAudioButtonBack).addClass("playing");
                    $(presenter.flashcardsCardAudioButtonBack).removeClass("paused");
                    presenter.sendPlayEvent();
                }else{
                    if (command == "stop" &amp;&amp; presenter.audioElementBack.currentTime &gt; 0) {
                        presenter.audioElementBack.currentTime = 0;
                        presenter.sendEndedEvent();
                        $(presenter.flashcardsCardAudioButtonBack).removeClass("paused");
                    }
                    if (presenter.isBackPlaying == false) return;
                    presenter.isBackPlaying = false;
                    presenter.audioElementBack.pause();
                    $(presenter.flashcardsCardAudioButtonBack).removeClass("playing");
                    if (command == "pause") {
                        $(presenter.flashcardsCardAudioButtonBack).addClass("paused");
                        presenter.sendPauseEvent();
                    }
                }
            }
        }
    }

    presenter.setWCAGStatus = function(isOn) {
        isWCAGOn = isOn;
    }

    presenter.removeActiveElementClass = function() {
        presenter.$view.find("."+KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS).removeClass(KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS);
    }

    presenter.nextKeyboardElement = function() {
        cycleKeyboardElement(true);
    }

    presenter.prevKeyboardElement = function() {
        cycleKeyboardElement(false);
    }

    function cycleKeyboardElement(forward) {
        let elementList = [presenter.flashcardsMain, presenter.flashcardsButtonFavourite];
        let currentElementIndex = 0;
        if (presenter.flashcardsMain.is('.flashcards-card-reversed')) {
            elementList.push(presenter.flashcardsCardAudioButtonBack);
            elementList.push(presenter.flashcardsButtonWrong);
            elementList.push(presenter.flashcardsButtonReset);
            elementList.push(presenter.flashcardsButtonCorrect);
        } else {
            elementList.push(presenter.flashcardsCardAudioButtonFront);
        }

        elementList = elementList.filter( function($el) {
            return $el != null &amp;&amp; $el.length &gt; 0 &amp;&amp; $el.is(":visible");
        });

        for (let i = 0; i &lt; elementList.length; i++) {
            if (elementList[i].is("."+KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS)) {
                currentElementIndex = i;
                break;
            }
        }

        presenter.removeActiveElementClass();
        if (forward) {
            currentElementIndex += 1;
            if (currentElementIndex &gt;= elementList.length) currentElementIndex = elementList.length - 1;
        } else {
            currentElementIndex -= 1;
            if (currentElementIndex &lt; 0) currentElementIndex = 0;
        }
        elementList[currentElementIndex].addClass(KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS);

    }

    presenter.onSpace = function() {
        let currentElement = presenter.$view.find("."+KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS);
        if (currentElement.length === 0 || currentElement.hasClass("flashcards-card")) {
            presenter.removeActiveElementClass();
            presenter.revertCard();
            speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.turned)]);
        } else {
            currentElement.click();
            if (currentElement.hasClass('flashcards-button-reset')) {
                speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.cardHasBeenReset)]);
            } else if (currentElement.hasClass('flashcards-button-correct')
                || currentElement.hasClass('flashcards-button-wrong')) {
                speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selected)]);
            } else if (currentElement.hasClass('flashcards-button-favourite')) {
                if (currentElement.hasClass('flashcards-button-selected')) {
                    speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selected)]);
                } else {
                    speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.deselected)]);
                }
            }
        }
    }

    presenter.readSelected = function() {
        let textVoices = [];
        let currentElement = presenter.$view.find("."+KEYBOARD_NAVIGATION_ACTIVE_ITEM_CLASS);
        if (currentElement.length === 0 || currentElement.hasClass("flashcards-card")) {
            presenter.readCard();
        } else {
            if (currentElement.hasClass('flashcards-button-favourite')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.favourite));
            } else if (currentElement.hasClass('flashcards-card-audio-button')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.audio));
            } else if (currentElement.hasClass('flashcards-button-wrong')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.wrong));
            } else if (currentElement.hasClass('flashcards-button-correct')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.correct));
            } else if (currentElement.hasClass('flashcards-button-reset')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.reset));
            }
            if (currentElement.hasClass('flashcards-button-selected')) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selected));
            }
            speak(textVoices);
        }
    }

    presenter.readCard = function() {
        let $content = null;
        if (presenter.flashcardsMain.is('.flashcards-card-reversed')) {
            $content = presenter.flashcardsMain.find('.flashcards-card-contents-back');
        } else {
            $content = presenter.flashcardsMain.find('.flashcards-card-contents-front');
        }

        let textVoices = window.TTSUtils.getTextVoiceArrayFromElement($content, presenter.configuration.langTag);
        speak(textVoices);
    }

    presenter.readCardNumber = function() {
        let textVoices = [];
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.card));
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.state.currentCard + ''));
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.outOf));
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.state.totalCards + ''));
        speak(textVoices);
    }

    presenter.keyboardController = function(keycode, isShift, event) {
        event.preventDefault();
        switch (keycode) {
            case 9: // TAB
                if (isShift) {
                    presenter.prevKeyboardElement();
                } else {
                    presenter.nextKeyboardElement();
                }
                presenter.readSelected();
                break;
            case 13: //ENTER
                if (isShift) {
                    presenter.removeActiveElementClass();
                } else {
                    presenter.readSelected();
                }
                break;
            case 32: // SPACE
                presenter.onSpace();
                break;
            case 38: // UP
                presenter.prevCard();
                presenter.readCardNumber();
                break;
            case 40: // DOWN
                presenter.nextCard();
                presenter.readCardNumber();
                break;
            case 37: // LEFT
                presenter.prevCard();
                presenter.readCardNumber();
                break;
            case 39: // RIGHT
                presenter.nextCard();
                presenter.readCardNumber();
                break;
            case 27: // ESC
                presenter.removeActiveElementClass();
                break;
        }
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.renderMathJax = function () {
        if (presenter.isLoaded) {
            reloadMathJax();
        }
    }

    function reloadMathJax () {
        const args = [];
        args.push("Typeset", MathJax.Hub, presenter.$view[0]);
        MathJax.Hub.Queue(args);
    }

    function parseAltText(text) {
        return presenter.isPreview ? window.TTSUtils.parsePreviewAltText(text) : presenter.textParser.parseAltTexts(text);
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Fractions" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Figure" nameLabel="Fractions_property_figure" type="{---,Rectangular,Circle,Square}"/>
		<property displayName="Rectangular horizontal parts" name="RectHorizontal" nameLabel="Fractions_property_rect_horizontal" type="string"/>
		<property displayName="Rectangular vertical parts" name="RectVertical" nameLabel="Fractions_property_rect_vertical" type="string"/>
		<property displayName="Circle parts" name="CircleParts" nameLabel="Fractions_property_circle_parts" type="string"/>
		<property displayName="Square parts" name="SquareParts" nameLabel="Fractions_property_square_parts" type="string"/>
		<property displayName="Number of correct parts" name="Correct" nameLabel="Fractions_property_correct" type="string"/>
		<property displayName="Selected parts" name="selectedParts" nameLabel="Fractions_property_selected_parts" type="string"/>
		<property displayName="Selection color" name="selectionColor" nameLabel="Fractions_property_selection_color" type="string"/>
		<property displayName="Empty color" name="emptyColor" nameLabel="Fractions_property_empty_color" type="string"/>
		<property displayName="Stroke color" name="strokeColor" nameLabel="Fractions_property_stroke_color" type="string"/>
		<property displayName="Stroke width" name="strokeWidth" nameLabel="Fractions_property_stroke_width" type="string"/>
		<property name="isNotActivity" nameLabel="Fractions_property_is_not_activity" type="boolean"/>
		<property name="isDisable" nameLabel="Fractions_property_is_disable" type="boolean"/>
        <property displayName="Image for deselected parts" name="imageDeselect" nameLabel="Fractions_property_is_deselect" type="image"/>
        <property displayName="Image for selected parts" name="imageSelect" nameLabel="Fractions_property_is_select" type="image"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="Fractions_property_speech_texts" type="staticlist">
			<property name="Selected" nameLabel="Fractions_speech_text_selected" type="staticrow">
				<property name="Selected" nameLabel="Fractions_speech_text_selected" type="string"/>
			</property>
			<property name="Deselected" nameLabel="Fractions_speech_text_deselected" type="staticrow">
				<property name="Deselected" nameLabel="Fractions_speech_text_deselected" type="string"/>
			</property>
			<property name="Item" nameLabel="Fractions_speech_text_item" type="staticrow">
				<property name="Item" nameLabel="Fractions_speech_text_item" type="string"/>
			</property>
			<property name="Correct" nameLabel="Fractions_speech_text_correct" type="staticrow">
				<property name="Correct" nameLabel="Fractions_speech_text_correct" type="string"/>
			</property>
			<property name="Wrong" nameLabel="Fractions_speech_text_wrong" type="staticrow">
				<property name="Wrong" nameLabel="Fractions_speech_text_wrong" type="string"/>
			</property>
			<property name="of" nameLabel="Fractions_speech_text_of" type="staticrow">
				<property name="of" nameLabel="Fractions_speech_text_of" type="string"/>
			</property>
		</property>
	</model>
<css>.FractionsWrapper{
    width: 100%;
    height: 100%;
}
</css><view>&lt;p class="FractionsCommandsViewer" style="position: absolute;"&gt;Choose the figure.&lt;/p&gt;
&lt;div class="FractionsWrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;p class="FractionsCommandsViewer" style="position: absolute;"&gt;Choose the figure.&lt;/p&gt;
&lt;div class="FractionsWrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonFractions_create(){

    var presenter = function () {};

    presenter.currentSelected = function(){};
    var Counter = 0;
    presenter.isErrorCheckingMode = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.currentSelected.item = [];
    presenter.isVisible = '';
    presenter.wasVisible = '';
    presenter.playerController = null;
    presenter.isDrawn = false;
    presenter.initialMarks = 0;
    presenter.validate = false;
    presenter.isDisable = false;
    presenter.wasDisable = false;
    presenter.imageBackgroundTable = ["0"];

    presenter.isWCAGOn = false;
    presenter.keyboardControllerObject = null;
    presenter.markedItemColor = '#00ff44';
    presenter.markedItemBorderData = '8,4';
    presenter.markedItemIndex = -1;
    presenter.orderItemIndex = -1;
    presenter.isWCAGOn = false;
    presenter.isFirstEnter = true;

    presenter.DEFAULT_TTS_PHRASES = {
        SELECTED: "selected",
        DESELECTED: "deselected",
        ITEM: "item",
        CORRECT: "correct",
        WRONG: "wrong",
        OF: "of",
    };

    presenter.ERROR_CODES = {
        "Z01": "None",
        "C01": "Incorrect selectionColor.",
        "C02": "Incorrect strokeColor.",
        "C03": "Incorrect emptyColor.",
        "C04": "Incorrect strokeWidth.",
        "P01": 'Incorrect horizontal rectangular parts.',
        "P02": 'Incorrect vertical rectangular parts.',
        "P03": 'Choose rectangular parts less than 30.',
        "P04": 'Incorrect circle parts.',
        "P05": 'Choose circle parts less than 100.',
        "P06": 'Enter valid square parts value.',
        "P07": 'Square parts value must be a power of 2.',
        "A01": 'Incorrect value for "Number of correct parts" property.',
        "A02": 'Fill the "Number of correct parts", or check isNotActivity.',
        "F01": 'Wrong figure name'
    };

    presenter.FIGURES = {
        CIRCLE: 1,
        RECTANGULAR: 2,
        SQUARE: 3
    };

    presenter.configuration = {
        modelIsValid: false,
        errorCode: "Z01"
    };

    function displayText() {
        var textToDisplay = presenter.model['Text to be displayed'],
            isTextColored = presenter.model['Color text'] === 'True',
            $textContainer = presenter.$view.find('.some-text-container');

        $textContainer.text(textToDisplay);
        if (isTextColored) {
            $textContainer.css('color', 'red');
        }
    }

    presenter.setVisibility = function(isVisible) {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.disable = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();

        presenter.isDisable = true;
        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        $($myDiv).addClass('disable');
    };

    presenter.enable = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();

        presenter.isDisable = false;
        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        $($myDiv).removeClass('disable');
    };

    presenter.isAttempted = function(){
        if(!presenter.configuration.isAnswer) {
            return true;
        }
        return Counter === (presenter.initialMarks)/2 ? false : true;
    };

    function getDefaultImageURL(isPreview) {
        var urlPrefix = isPreview ? '/media/iceditor/' : presenter.playerController.getStaticFilesPath();

        return urlPrefix + "addons/resources/fractions-default-image.png"
    }

    function generateValidationError(code) {
        return {
            isValid: false,
            errorCode: code
        };
    }

    presenter.validateColors = function (model) {
        var selectionColor = '#7FFFD4';
        var strokeColor= '#838B8B';
        var emptyColor = '#eeeeee';
        var strokeWidth = undefined;

        if(model.selectionColor.length &gt; 0) {
            selectionColor = model.selectionColor;
        }

        if(model.strokeColor.length &gt; 0) {
            strokeColor = model.strokeColor;
        }

        if(model.emptyColor.length &gt; 0) {
            emptyColor = model.emptyColor;
        }

        if(model.strokeWidth.length &gt; 0 ) {
            strokeWidth = model.strokeWidth;
        }

        if(!(presenter.checkColor(selectionColor))) {
            return generateValidationError("C01");
        }
        else if(!(presenter.checkColor(strokeColor))) {
            return generateValidationError("C02");
        }
        else if(!(presenter.checkColor(emptyColor))) {
            return generateValidationError("C03");
        }
        else if (isNaN(parseInt(strokeWidth))) {
            if (strokeWidth === undefined) {
                strokeWidth = 1;
            } else {
                return generateValidationError("C04");
            }
        }

        return  {
            isValid: true,
            selectionColor: selectionColor,
            strokeColor: strokeColor,
            emptyColor: emptyColor,
            strokeWidth: strokeWidth
        };
    };

    presenter.validateRectangleParts = function (model) {
        if(parseFloat(model.RectHorizontal) == 0 || parseFloat(model.RectHorizontal) != parseInt(model.RectHorizontal)) {
            return generateValidationError("P01");
        }

        if (parseFloat(model.RectVertical) == 0 || parseFloat(model.RectVertical) != parseInt(model.RectVertical)) {
            return generateValidationError("P02");
        }

        if(parseFloat(model.RectHorizontal) &gt; 30 || parseFloat(model.RectVertical) &gt; 30){
            return generateValidationError("P03");
        }

        return {
            isValid: true,
            rectHorizontal: parseInt(model.RectHorizontal),
            rectVertical: parseInt(model.RectVertical)
        };
    };

    presenter.validateCircleParts = function (model) {
        if(parseInt(model.CircleParts) != parseFloat(model.CircleParts) || parseFloat(model.CircleParts) &lt;= 0 || isNaN(model.CircleParts)) {
            return generateValidationError("P04");
        }
        if(parseFloat(model.CircleParts) &gt; 100){
            return generateValidationError("P05");
        }

        return {
            isValid: true,
            circleParts: parseInt(model.CircleParts)
        };
    };

    function log2(number) {
        return Math.log(number) / Math.log(2);
    }

    presenter.validateSquareParts = function (model) {
        if (parseInt(model.SquareParts) &lt;= 0 || isNaN(model.SquareParts)) {
            return generateValidationError("P06");
        }

        if (log2(parseInt(model.SquareParts)) % 1 != 0) {
            return generateValidationError("P07");
        }

        return {
            isValid: true,
            squareParts: parseInt(model.SquareParts)
        };
    };

    presenter.validateParts = function (model, figure) {
        var validatedRectangleParts = presenter.validateRectangleParts(model);
        if (figure == presenter.FIGURES.RECTANGULAR) {
            if (!validatedRectangleParts.isValid) {
                return validatedRectangleParts;
            }
        }

        var validatedCircleParts = presenter.validateCircleParts(model);
        if (figure == presenter.FIGURES.CIRCLE) {
            if (!validatedCircleParts.isValid) {
                return validatedCircleParts;
            }
        }

        var validatedSquareParts = presenter.validateSquareParts(model);
        if (figure == presenter.FIGURES.SQUARE) {
            if (!validatedSquareParts.isValid) {
                return validatedSquareParts;
            }
        }

        return {
            isValid: true,
            rectHorizontal: validatedRectangleParts.rectHorizontal || 0,
            rectVertical: validatedRectangleParts.rectVertical || 0,
            circleParts: validatedCircleParts.circleParts || 0,
            squareParts: validatedSquareParts.squareParts || 0
        };
    };

    presenter.validateCorrectAnswer = function (model) {
        var isNotActivity = ModelValidationUtils.validateBoolean(model.isNotActivity);

        if (isNotActivity) {
            return {
                isValid: true,
                isAnswer: false
            };
        }

        var correctAnswer = model.Correct;
        if (correctAnswer.length &gt; 0) {
            if (
                isNaN(correctAnswer)
                || parseFloat(correctAnswer) != Math.round(correctAnswer)
                || parseFloat(correctAnswer) &lt;= 0
                || parseFloat(correctAnswer) &gt; parseInt(model.RectHorizontal, 10) * parseInt(model.RectVertical, 10)
                || parseFloat(correctAnswer) &gt; parseInt(model.SquareParts, 10)
                || parseFloat(correctAnswer) &gt; parseInt(model.CircleParts, 10)) {

                return generateValidationError("A01");
            }

            return {
                isValid: true,
                isAnswer: true,
                correctAnswer: parseInt(correctAnswer)
            };
        } else {
            if (!isNotActivity) {
                return generateValidationError("A02")
            }
        }
    };

    presenter.validateFigure = function (model) {
        var figure = model.Figure;
        var figureCode = presenter.FIGURES[figure.toUpperCase()];
        if (figureCode === undefined) {
            return generateValidationError("F01");
        }

        return {
            isValid: true,
            figure: figureCode
        }
    };

    presenter.validateModel = function (model, isPreview) {

        var imageSelectChecker = false;
        var imageDeselectChecker = false;
        var imageSelect = undefined;
        var imageDeselect = undefined;

        var selected = {
            selectedString: undefined,
            haveSelectedElements: false
        };

        if(model.selectedParts.length &gt; 0) {
            selected.selectedString = model.selectedParts;
            selected.haveSelectedElements = true;
        }
        else {
            selected.selectedString = '';
        }

        if (model['imageSelect'] == undefined || model['imageSelect'].length == 0) {
            imageSelect = getDefaultImageURL(isPreview);
        } else {
            imageSelectChecker = true;
            imageSelect = model.imageSelect;
        }

        if (model['imageDeselect'] == undefined || model['imageDeselect'].length == 0) {
            imageDeselect = getDefaultImageURL(isPreview);
        } else{
            imageDeselectChecker = true;
            imageDeselect = model.imageDeselect;
        }

        var validatedColors = presenter.validateColors(model);
        if (!validatedColors.isValid) {
            return validatedColors;
        }

        var validatedFigure = presenter.validateFigure(model);
        if (!validatedFigure.isValid) {
            return validatedFigure;
        }

        var validatedParts = presenter.validateParts(model, validatedFigure.figure);
        if (!validatedParts.isValid) {
            return validatedParts;
        }

        var validatedAnswer = presenter.validateCorrectAnswer(model);
        if (!validatedAnswer.isValid) {
            return validatedAnswer;
        }

        return {
            isValid: true,
            imageSelect: imageSelect,
            imageDeselect: imageDeselect,
            imageSelectChecker: imageSelectChecker,
            imageDeselectChecker: imageDeselectChecker,
            selected: selected,
            selectionColor: validatedColors.selectionColor,
            strokeColor: validatedColors.strokeColor,
            emptyColor: validatedColors.emptyColor,
            strokeWidth: parseFloat(validatedColors.strokeWidth),
            rectHorizontal: validatedParts.rectHorizontal,
            rectVertical: validatedParts.rectVertical,
            circleParts: validatedParts.circleParts,
            squareParts: validatedParts.squareParts,
            isAnswer: validatedAnswer.isAnswer,
            correctAnswer: validatedAnswer.correctAnswer,
            figure: validatedFigure.figure,
            addonWidth: parseInt(model.Width, 10),
            addonHeight: parseInt(model.Height, 10),
            addonId: model.ID,
            isNotActivity: ModelValidationUtils.validateBoolean(model.isNotActivity)
        };
    };

    presenter.init = function(view, model, isPreview){
        presenter.$view = $(view);
        presenter.model = model;

        var radius = 0;
        var circOX = 0;
        var circOY = 0;

        var config = presenter.configuration;
        var myDiv =  $(view).find('.FractionsWrapper')[0];

        var i = 0;

        if(presenter.configuration.figure == presenter.FIGURES.RECTANGULAR) {
            for(i = 0; i &lt; config.rectHorizontal * config.rectVertical + 1; i++){
                presenter.currentSelected.item[i] = false;
            }
            presenter.currentSelected.item[0] = model.ID;

            var figureRect = presenter.drawRect();
            $(myDiv).append(figureRect);
            $(myDiv).addClass('rect');
            presenter.isDrawn = true;
            presenter.validate = true;
        }

        if(presenter.configuration.figure == presenter.FIGURES.CIRCLE) {
            for(i = 0; i &lt; config.circleParts + 1; i++){
                presenter.currentSelected.item[i] = false;
            }
            presenter.currentSelected.item[0] = model.ID;

            if(config.addonHeight &gt;= config.addonWidth){
                radius = Math.round((config.addonWidth - 2 * config.strokeWidth) * 50) / 100;
                circOX = radius + config.strokeWidth;
                circOY = radius + config.strokeWidth;
            }
            else {
                radius = Math.round((config.addonHeight - 2 * config.strokeWidth) * 50) / 100;
                circOX = radius + config.strokeWidth;
                circOY = radius + config.strokeWidth;
            }

            var figureCirc = presenter.drawArcs(circOX,circOY, radius);
            $(myDiv).append(figureCirc);
            $(myDiv).addClass('circ');
            presenter.isDrawn = true;
            presenter.validate = true;

        }

        if (presenter.configuration.figure == presenter.FIGURES.SQUARE) {
            presenter.buildSquare(model, view);
        }

        if(model.isDisable == 'True') {
            presenter.isDisable = true;
            presenter.wasDisable = true;
            $(myDiv).addClass('disable');
        }

        presenter.clear();

        if (presenter.configuration.selected.haveSelectedElements) {
            presenter.selected(presenter.configuration.selected.selectedString);
        }

        displayText();
        presenter.buildKeyboardController();
    };

    presenter.buildSquare = function (model, view) {
        var config = presenter.configuration;
        var parts = config.squareParts;
        var addonWidth = config.addonWidth;
        var addonHeight = config.addonHeight;
        var squareSize = Math.min(addonWidth, addonHeight);
        var i;

        for(i = 0; i &lt; parts + 1; i++){
            presenter.currentSelected.item[i] = false;
        }

        presenter.currentSelected.item[0] = model.ID;

        var d = '&lt;svg xmlns="http://www.w3.org/2000/svg" ' +
            'version="1.1"  ' +
            'width="' + squareSize + '" ' +
            'height="' + squareSize + '"&gt;';

        var elements = [new SquareShapeElement(addonWidth, addonHeight, 0,0, 1, config.addonId)];

        //Cut elements to half Log2(n) times
        for (i = 1; i &lt;= log2(parts); i++) {
            var elementsBuff = [];
            for (var elementIndex = 0; elementIndex &lt; elements.length; elementIndex++) {
                elementsBuff = elementsBuff.concat(elements[elementIndex].cutToHalf(addonWidth, addonHeight));
            }
            elements = elementsBuff;
        }
        d += '&lt;rect ' +
            'id="myBorder" ' +
            'height="'+(squareSize - (2 * config.strokeWidth)) + '" ' +
            'width="'+ (squareSize - (2 * config.strokeWidth)) + '" ' +
            'y="' + config.strokeWidth + '" ' +
            'x="' + config.strokeWidth + '" ' +
            'stroke-width="' + config.strokeWidth + '" ' +
            'stroke="' + config.strokeColor+'" ' +
            'style="fill: ' + config.strokeColor + '"/&gt;';

        for (i = 0; i &lt; elements.length; i++) {
            elements[i].calculateValues(addonWidth, addonHeight);
            d += elements[i].getSVGString(i + 1);
        }

        d += '&lt;/svg&gt;';

        var myDiv =  $(view).find('.FractionsWrapper')[0];
        $(myDiv).append(d);
        $(myDiv).addClass('rect');
        presenter.isDrawn = true;
        presenter.validate = true;
    };

    presenter.checkColor = function(color) {

        var regExp = new RegExp("^#[0-9a-fA-F]{6}$");
        var colorMatch;
        colorMatch = color.match(regExp);

        if(colorMatch === null) {
            return false;
        }
        else {
            return true;
        }
    };

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'setSelectionColor'.toLowerCase():
                presenter.setSelectionColorButton(params);
                break;
            case 'getCurrentNumber'.toLowerCase():
                presenter.getCurrentNumber();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'markAsCorrect'.toLowerCase():
                presenter.markAsCorrect();
                break;
            case 'markAsWrong'.toLowerCase():
                presenter.markAsWrong();
                break;
            case 'markAsEmpty'.toLowerCase():
                presenter.markAsEmpty();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'showElementsSA'.toLowerCase():
                presenter.showElementsSA(params[0]);
                break;
            case 'hideElementsSA'.toLowerCase():
                presenter.hideElementsSA();
                break;
            case 'allElements'.toLowerCase():
                presenter.allElements();
                break;
            case 'isErrorCheckMode'.toLowerCase():
                presenter.isErrorCheckMode(params[0]);
                break;
            case 'addShowAnswersClass'.toLowerCase():
                presenter.addShowAnswersClass();
                break;
            case 'removeShowAnswersClass'.toLowerCase():
                presenter.removeShowAnswersClass();
                break;
            case 'getCurrentNumberSA'.toLowerCase():
                presenter.getCurrentNumberSA();
                break;
            case 'isAllOK'.toLowerCase():
                presenter.isAllOK();
                break;

        }
    };

    presenter.allElements = function(){
        return presenter.allElementsCount;
    };

    presenter.addShowAnswersClass = function(){
        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        $($myDiv).addClass('showAnswers');
    };

    presenter.removeShowAnswersClass = function(){
        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        $($myDiv).removeClass('showAnswers');
    };

    presenter.isErrorCheckMode = function(value){
        presenter.isErrorCheckingMode = value;
    };

    presenter.markAsCorrect = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        presenter.isErrorCheckingMode = true;
        $($myDiv).removeClass('incorrect');
        $($myDiv).addClass('correct');
    };

    presenter.markAsWrong = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        presenter.isErrorCheckingMode = true;
        $($myDiv).removeClass('correct');
        $($myDiv).addClass('incorrect');
    };

    presenter.markAsEmpty = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        presenter.isErrorCheckingMode = false;
        $($myDiv).removeClass('incorrect');
        $($myDiv).removeClass('correct');
    };

    presenter.setSelectionColor = function(color){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.configuration.selectionColor = color;
    };

    presenter.setSelectionColorButton = function(color){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.configuration.selectionColor = color[0];
    };

    presenter.getCurrentNumber = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        return Counter;
    };

    presenter.isAllOK = function(){
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        if(!presenter.configuration.isAnswer) {
            return true;
        }
        return Counter == presenter.configuration.correctAnswer ? true : false;
    };

    presenter.getCurrentNumberSA = function(){
        return Counter;
    };

    presenter.run = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        var $counter = undefined;

        $.extend(presenter.configuration, presenter.validateModel(model, false));
        presenter.setSpeechTexts(model['speechTexts']);
        if (!presenter.configuration.isValid) {
            $counter = $(view).find('.FractionsCommandsViewer');
            $counter.text(presenter.ERROR_CODES[presenter.configuration.errorCode]);
        } else {
            $counter = $(view).find('.FractionsCommandsViewer');
            $counter.text('');

            presenter.init(view, model, false);
            if (model.Figure == 'Rectangular') {
                presenter.allElementsCount = model.RectHorizontal * model.RectVertical;
            }

            if (model.Figure == 'Circle') {
                presenter.allElementsCount = model.CircleParts;
            }

            presenter.isVisible = model["Is Visible"] == 'True';
            presenter.wasVisible = model["Is Visible"] == 'True';
            presenter.setVisibility(presenter.isVisible);

            const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
            for (let i = 0; i &lt; events.length; i++) {
                presenter.eventBus.addEventListener(events[i], this);
            }

            $(view).find('path').click(function (e) {
                presenter.markElementAsClicked(this);
                e.stopPropagation();
            });

            jQuery(function ($) {
                $(view).find('path').on("mouseenter", function () {
                    var classString = $(this).attr('class');
                    var newClass = classString + " mouse-hover";
                    $(this).attr('class', newClass);
                });
            });

            jQuery(function ($) {
                $(view).find('path').on("mouseleave", function () {
                    var classString = $(this).attr('class');
                    var mouseLeaveClass = classString.replace(' mouse-hover', '');
                    $(this).attr("class", mouseLeaveClass);
                });

            });
        }

    };

    presenter.createPreview = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        var $myDiv =  presenter.$view.find('path');
        if(presenter.isDrawn) $($myDiv).remove();

        $.extend(presenter.configuration, presenter.validateModel(model, true));
        if (presenter.configuration.isValid === false) {
            var $counter = $(view).find('.FractionsCommandsViewer');
            $counter.text(presenter.ERROR_CODES[presenter.configuration.errorCode]);
        } else {
            var $counter = $(view).find('.FractionsCommandsViewer');
            $counter.text('');
            presenter.init(view, model, true);
        }
    };

    presenter.clear = function(){
        var $myDiv, i;
        if(presenter.imageBackgroundTable[0] == 0){

            for(i = 0; i &lt; presenter.currentSelected.item.length - 1; i++){
                presenter.imageBackgroundTable[i] = presenter.$view.find('"#'+presenter.currentSelected.item[0]+'imageBackground' +  (i+1) + '"');
            }
        }
        if(presenter.configuration.imageSelectChecker || presenter.configuration.imageDeselectChecker){
            for(i = 0; i &lt; presenter.currentSelected.item.length - 1; i++){
                $myDiv =  presenter.$view.find('"#' + presenter.currentSelected.item[0] + (i+1) + '"');
                jQuery($myDiv).css('fill', presenter.configuration.emptyColor);
                jQuery($myDiv).removeClass("selected");
                presenter.imageBackgroundTable[i].attr("xlink:href", presenter.configuration.imageDeselect);
            }
        }else {
            for(i = 0; i &lt; presenter.currentSelected.item.length;i++){
                $myDiv = presenter.$view.find('path')[i];
                $($myDiv).removeClass("selected");
                $($myDiv).css('fill', presenter.configuration.emptyColor);
            }

        }
        return true;
    };

    presenter.selected = function(selectedString){

        if(presenter.validate){
            presenter.clear();
            if(selectedString.indexOf(',') !== -1){
                var selectedStringBufor = selectedString;
                while(selectedStringBufor.indexOf(',') !== -1) {
                    var position = selectedStringBufor.indexOf(',');
                    var toSelect = selectedStringBufor.slice(0, position);
                    selectedStringBufor = selectedStringBufor.slice(position + 1, selectedStringBufor.length);

                    if(!(toSelect.isNaN) &amp;&amp; parseFloat(toSelect) === Math.round(toSelect)){
                        presenter.markElementAsSelected(toSelect);
                        presenter.currentSelected.item[toSelect] = true;
                        Counter++;
                        presenter.initialMarks++;
                    } else {
                        presenter.selectRange(toSelect);
                    }
                }
                var toSelect1 = selectedStringBufor;
                if(!(toSelect1.isNaN) &amp;&amp; parseFloat(toSelect1) === Math.round(toSelect1)){
                    presenter.markElementAsSelected(toSelect1);
                    presenter.currentSelected.item[toSelect1] = true;
                    Counter++;
                    presenter.initialMarks++;
                } else {
                    presenter.selectRange(toSelect1);
                }
            } else {
                var toSelect2 = selectedString;
                if(!(toSelect2.isNaN) &amp;&amp; parseFloat(toSelect2) === Math.round(toSelect2)){
                    presenter.markElementAsSelected(toSelect2);
                    presenter.currentSelected.item[toSelect2] = true;
                    Counter++;
                    presenter.initialMarks++;
                } else {
                    presenter.selectRange(toSelect2);
                }
            }
        }
    };

    presenter.selectRange = function(selectedString){

        var toSelect3 = 1;
        if(selectedString.indexOf('-') !== -1){
            var selectedStringBufor = selectedString;
            var position1 = selectedStringBufor.indexOf('-');
            var toSelect1 = selectedStringBufor.slice(0, position1);

            selectedStringBufor = selectedStringBufor.slice(position1 + 1, selectedStringBufor.length);

            if(selectedStringBufor.indexOf('-') !== -1){
                var position2 = selectedStringBufor.indexOf('-');
                var toSelect2 = selectedStringBufor.slice(0, position2);
                selectedStringBufor = selectedStringBufor.slice(position2 + 1, selectedStringBufor.length);
                toSelect3 = selectedStringBufor;
            } else {
                var toSelect2 = selectedStringBufor.slice(0, selectedStringBufor.length);
            }

            if(!(toSelect1.isNaN) &amp;&amp; parseFloat(toSelect1) === Math.round(toSelect1) &amp;&amp; !(toSelect2.isNaN) &amp;&amp; parseFloat(toSelect2) === Math.round(toSelect2) &amp;&amp; !(toSelect3.isNaN) &amp;&amp; parseFloat(toSelect3) === Math.round(toSelect3))
            {
                if(parseFloat(toSelect1) &gt; presenter.currentSelected.item.length) {
                    toSelect1 = presenter.currentSelected.item.length;
                }
                if(parseFloat(toSelect2) &gt; presenter.currentSelected.item.length) {
                    toSelect2 = presenter.currentSelected.item.length;
                }
                if(parseFloat(toSelect3) &gt; presenter.currentSelected.item.length) {
                    toSelect3 = presenter.currentSelected.item.length;
                }

                if(presenter.validate){
                    for(var i = parseFloat(toSelect1); i &lt; parseFloat(toSelect2) + 1;i+=parseFloat(toSelect3)){
                        presenter.markElementAsSelected(i);
                        presenter.currentSelected.item[i] = true;
                        Counter++;
                        presenter.initialMarks++;
                    }
                }
            }
        }
    };


    presenter.markElementAsClicked = function(element){
        var correctAnswer = presenter.configuration.correctAnswer;
        var clickedElementID = element.id;
        if (!presenter.isErrorCheckingMode
            &amp;&amp; !presenter.isShowAnswersActive
            &amp;&amp; !presenter.isGradualShowAnswersActive
            &amp;&amp; !presenter.isDisable)
        {
            if(presenter.currentSelected.item[clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length)] === false)
            {
                element.style.fill = presenter.configuration.selectionColor;
                $(element).addClass("selected");
                Counter++;
                if(presenter.configuration.imageDeselectChecker || presenter.configuration.imageSelectChecker){
                    presenter.imageBackgroundTable[clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length) - 1].attr("xlink:href", presenter.configuration.imageSelect);
                }
                presenter.currentSelected.item[clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length)] = true;
                presenter.triggerFrameChangeEvent(clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length),1,Counter == correctAnswer ? 1 : 0);
            } else   {
                element.style.fill = presenter.configuration.emptyColor;
                $(element).removeClass("selected");
                presenter.currentSelected.item[clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length)] = false;
                Counter--;
                presenter.triggerFrameChangeEvent(clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length),0,Counter == correctAnswer ? 1 : 0);

                if(presenter.configuration.imageDeselectChecker || presenter.configuration.imageSelectChecker){
                    presenter.imageBackgroundTable[clickedElementID.slice(presenter.currentSelected.item[0].length,clickedElementID.length) - 1].attr("xlink:href", presenter.configuration.imageDeselect);
                }

            }
        }
    };

    presenter.markElementAsSelected = function(element){
        var $myDiv =  presenter.$view.find('"#' +  presenter.currentSelected.item[0] + element + '"');
        jQuery($myDiv).css('fill', presenter.configuration.selectionColor);
        presenter.initialMarks++;
        jQuery($myDiv).addClass("selected");
        if(presenter.configuration.imageSelectChecker || presenter.configuration.imageDeselectChecker){
            presenter.imageBackgroundTable[element - 1].attr("xlink:href", presenter.configuration.imageSelect);
        }
    };


    presenter.drawRect = function() {
        var config = presenter.configuration;

        var id = config.addonId;
        var addonWidth = config.addonWidth;
        var addonHeight = config.addonHeight;
        var partsHorizontally = config.rectHorizontal;
        var partsVertically = config.rectVertical;

        var elementHeight = parseFloat(addonHeight) - (2 * config.strokeWidth);
        var elementWidth = parseFloat(addonWidth) - (2 * config.strokeWidth);

        var stepX = (addonWidth - (2 * config.strokeWidth)) /partsHorizontally;
        var stepY = (addonHeight - (2 * config.strokeWidth)) /partsVertically;

        var fig = '&lt;svg xmlns="http://www.w3.org/2000/svg" version="1.1"  width="' + addonWidth+'" height="' + addonHeight+'"&gt;';
        fig += '&lt;rect ' +
            'id="myBorder" ' +
            'height="'+ elementHeight + '" ' +
            'width="' + elementWidth + '" ' +
            'y="' + config.strokeWidth + '" ' +
            'x="' + config.strokeWidth + '" ' +
            'stroke-width="' + config.strokeWidth + '" ' +
            'stroke="' + config.strokeColor+'" ' +
            'style="fill: none"/&gt;';

        var k =1;

        for(var j=0;j&lt;partsVertically ;j++) {
            for(var i=0;i&lt;partsHorizontally ;i++) {
                fig += '&lt;path ' +
                        'id="'+ id + k +'" ' +
                        'class="' + id + '" ' +
                        'd="M' + (config.strokeWidth + i * stepX) + ',' + (config.strokeWidth + j * stepY) + 'h' + stepX + ' v' +stepY + ' h' + (-stepX) +' v' + (-stepY) +'" ' +
                        'stroke-width="' + config.strokeWidth + '" ' +
                        'style="stroke: '+ config.strokeColor + '; fill: ' + config.emptyColor + ';" /&gt;';

                if(config.imageDeselectChecker || config.imageSelectChecker){
                    fig += '&lt;defs&gt;';

                    fig += '&lt;mask ' +
                        'id="' + id + 'mask' + k + '" ' +
                        'x="0" ' +
                        'y="0" ' +
                        'patternUnits="userSpaceOnUse" ' +
                        'height="100" ' +
                        'width="100"&gt;';

                    fig += '&lt;path ' +
                        'id="' + id + 'maskPath' + k + '" ' +
                        'class="image-path" ' +
                        'd="M' + (config.strokeWidth + i * stepX)+',' + (config.strokeWidth + j * stepY) + 'h' + stepX +'  v' + stepY + ' h' + (-stepX) +' v' + (-stepY) + '" ' +
                        'stroke-width="' + config.strokeWidth + '" ' +
                        'style="pointer-events:none;fill:#ffffff;" /&gt;';
                    fig += '&lt;/mask&gt;';
                    fig += '&lt;/defs&gt;';
                    fig += '&lt;image ' +
                        'y="' + j * stepY + '" ' +
                        'x="' + i * stepX + '" ' +
                        'id="' + id + 'imageBackground' + k + '" ' +
                        'class="image-background" ' +
                        'xlink:href="' + config.imageDeselect + '" ' +
                        'height="'+stepY+'" width="'+stepX+'" ' +
                        'style="pointer-events:none;"  mask="url(#' + id + 'mask' + k + ')"/&gt;';

                }
                k++;
            }
        }
        fig += '&lt;/svg&gt;';

        return fig;
    };


    presenter.drawArcs = function(centerX,centerY,radius){
        var parts = presenter.configuration.circleParts;
        var id = presenter.configuration.addonId;
        var addonWidth = presenter.configuration.addonWidth;
        var addonHeight = presenter.configuration.addonHeight;

        var step = parseInt(parts, 10) + 1;
        var sectorAngle = Math.round(360 / parts * 100) / 100;
        var angle = 360 - sectorAngle;

        if(parts == 1) {
            var d = '&lt;svg xmlns="http://www.w3.org/2000/svg" version="1.1"  width="' + addonWidth + '" height="' + addonHeight + '"&gt;';
            d += '&lt;path ' +
                'id="' + id + '1" ' +
                'd=" M ' + centerX + ', ' +centerY + 'm '+ (-radius) + ', 0 a ' + radius + ',' + radius + ' 0 1,0 ' + (2*radius)+ ',0 a ' + radius+ ',' + radius + ' 0 1,0 '+ (-2 * radius) + ',0" ' +
                'fill="' + presenter.configuration.emptyColor + '" ' +
                'stroke="' + presenter.configuration.strokeColor + '" ' +
                'stroke-width="' + presenter.configuration.strokeWidth + '" ' +
                'stroke-linejoin="round" /&gt;';
            d += '&lt;/svg&gt;';

            return d;
        } else{
            var d = '&lt;svg xmlns="http://www.w3.org/2000/svg" version="1.1"  width="' + addonWidth + '" height="' + addonHeight+'"&gt;';
            var x1 = Math.round((centerX + radius * Math.cos(Math.PI * angle / 180)) * 100) / 100;
            var y1 = Math.round((centerY + radius * Math.sin(Math.PI * angle / 180)) * 100) / 100;
            var x2 = Math.round((centerX + radius * Math.cos(Math.PI * angle / 180)) * 100) / 100;
            var y2 = Math.round((centerY + radius * Math.sin(Math.PI * angle / 180)) * 100) / 100;
            angle -= sectorAngle;
            d += '&lt;path ' +
                'id="' + id + '1" ' +
                'class="' + id + '" ' +
                'd="M' + centerX + ',' + centerY + 'l ' + radius + ', 0 A' + radius + ',' + radius + ' 0 0, 0 ' + (x1) + ',' + (y1) + ' z" ' +
                'fill="' + presenter.configuration.emptyColor + '" ' +
                'stroke="' + presenter.configuration.strokeColor + '" ' +
                'stroke-width="' + presenter.configuration.strokeWidth + '" ' +
                'stroke-linejoin="round" /&gt;';

            for(var j=2; j &lt; step ; j++){
                x1 = x2;
                y1 = y2;
                x2 = Math.round((centerX + radius*Math.cos(Math.PI * angle / 180)) * 100) / 100;
                y2 = Math.round((centerY + radius*Math.sin(Math.PI * angle / 180)) * 100) / 100;

                var stepX = x1 - centerX;
                var stepY = y1 - centerY;

                d += '&lt;path ' +
                    'id="' + id + j +'" ' +
                    'class="' + id + '" ' +
                    'd="M' + centerX + ',' + centerY + 'l ' + stepX + ',' + stepY +' A' + radius + ',' + radius + ' 0 0, 0 ' + (x2) + ',' + (y2) + ' z" ' +
                    'fill="' + presenter.configuration.emptyColor + '" ' +
                    'stroke="' + presenter.configuration.strokeColor + '" ' +
                    'stroke-width="' + presenter.configuration.strokeWidth + '" ' +
                    'stroke-linejoin="round" /&gt;';

                angle -= sectorAngle;
            }
            d += '&lt;/svg&gt;';
            return d;
        }
    };

    presenter.getState = function () {
        var currentItems = presenter.currentSelected.item;
        var visible = presenter.isVisible;
        var initialMarks = presenter.initialMarks;
        var wasDisable = presenter.wasDisable;
        var isDisable = presenter.isDisable;
        return JSON.stringify({
            Counter: Counter,
            currentItems: currentItems,
            visible: visible,
            initialMarks: initialMarks,
            wasDisable: wasDisable,
            isDisable: isDisable
        });
    };

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state),
            $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        presenter.isVisible = parsedState.visible;
        Counter = parsedState.Counter;
        presenter.wasDisable = parsedState.wasDisable;
        presenter.isDisable = parsedState.isDisable;
        presenter.setVisibility(presenter.isVisible);
        presenter.currentSelected.item = JSON.parse(state).currentItems;
        presenter.clear();

        for(var j = 1; j &lt; presenter.currentSelected.item.length; j++) {
            if(presenter.currentSelected.item[j] === true) {
                presenter.markElementAsSelected(j);
            }
        }
        presenter.initialMarks = parsedState.initialMarks;
        presenter.isDisable === true ?  $($myDiv).addClass('disable') : $($myDiv).removeClass('disable');
    };

    presenter.getMaxScore = function () {
        if(parseInt(presenter.initialMarks, 10) / 2 === presenter.configuration.correctAnswer) {
            return 0;
        }

        if(presenter.configuration.isAnswer) {
            return 1;
        } else {
            return 0;
        }
    };

    presenter.neutralOption = function(){
        return (Counter === (presenter.initialMarks) / 2) ? 1 : 0;
    };

    presenter.getScore = function () {
        if(parseInt(presenter.initialMarks, 10) / 2 === presenter.configuration.correctAnswer) {
            return 0;
        }

        if(presenter.configuration.isAnswer) {
            return (Counter == presenter.configuration.correctAnswer) ? 1 : 0;
        } else {
            return 0;
        }
    };

    presenter.getErrorCount = function () {
        if(presenter.configuration.isAnswer) {
            if(parseInt(presenter.initialMarks,10)/2 === presenter.configuration.correctAnswer &amp;&amp; presenter.configuration.correctAnswer != Counter) {
                return 1;
            }

            if(presenter.neutralOption() == 1) {
                return 0;
            }
            else {
                return presenter.getMaxScore() - presenter.getScore();
            }
        } else {
            return 0;
        }
    };

    presenter.setShowErrorsMode = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        presenter.isErrorCheckingMode = true;

        if(presenter.configuration.isAnswer) {
            var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];

            if(presenter.neutralOption() === 0) {
                if (presenter.getScore() === presenter.getMaxScore() &amp;&amp; presenter.getErrorCount() === 0) {
                    $($myDiv).addClass('correct');
                } else {
                    $($myDiv).addClass('incorrect');
                }
            }
        }
    };

    presenter.setWorkMode = function () {
        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        presenter.isErrorCheckingMode = false;
        $($myDiv).removeClass('correct');
        $($myDiv).removeClass('incorrect');
    };

    presenter.reset = function () {
        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();

        var $myDiv =  presenter.$view.find('.FractionsWrapper')[0];
        for(var i = 1; i&lt; presenter.currentSelected.item.length; i++) {
            presenter.currentSelected.item[i] = false;
        }
        Counter = 0;
        presenter.initialMarks = 0;
        presenter.selected(presenter.configuration.selected.selectedString);
        presenter.setVisibility(presenter.wasVisible);
        presenter.isDisable = presenter.wasDisable;
        presenter.isDisable === true ?  $($myDiv).addClass('disable') : $($myDiv).removeClass('disable');
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.setEventBus = function(eventBus) {
        presenter.eventBus = eventBus;
    };

    presenter.createEventData = function(partNumber,clickValue,checkScore) {
        return {
            source : presenter.currentSelected.item[0],
            item : "" + partNumber,
            value : '' + clickValue,
            score : '' + checkScore
        };
    };

    presenter.triggerFrameChangeEvent = function(partNumber,clickValue,checkScore,element) {
        var eventData = presenter.createEventData(partNumber,clickValue,checkScore);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.getActivitiesCount = function () {
        if (!presenter.configuration.isValid || presenter.configuration.isNotActivity) {
            return 0;
        }
        return 1;
    };

    presenter.markCorrectAnswerAsSelected = function(element){
        var $myDiv =  presenter.$view.find('"#' +  presenter.currentSelected.item[0] + element + '"');
        jQuery($myDiv).css('fill', presenter.configuration.selectionColor);
        jQuery($myDiv).addClass("selected");

        if(presenter.configuration.imageSelectChecker || presenter.configuration.imageDeselectChecker){
            presenter.imageBackgroundTable[element - 1].attr("xlink:href", presenter.configuration.imageSelect);
        }
    };

    presenter.isEnabledInGSAMode = function () {
        // Workaround. Prevents execution of presenter.enable and thus unlocking the addon when GradualHideAnswers is executed
        return presenter.isDisable;
    };

    presenter.showAnswers = function () {
        if (!presenter.configuration.isAnswer
            || presenter.configuration.isNotActivity) {
            return;
        }
        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isShowAnswersActive = true;

        showAnswersTo(presenter.configuration.correctAnswer);
    };

    presenter.showElementsSA = function(element){
        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isShowAnswersActive = true;

        showAnswersTo(element);
    };

    presenter.gradualShowAnswers = function (eventData) {
        if ((eventData.moduleID !== presenter.configuration.addonId)
            || !presenter.configuration.isAnswer
            || presenter.configuration.isNotActivity) {
            return;
        }

        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive = true;

        showAnswersTo(presenter.configuration.correctAnswer);
    };

    function showAnswersTo(number) {
        if (number != Counter){
            presenter.clear()
            let k = 0;
            if (Counter &lt; number){
                for (let j = 1; j &lt; presenter.currentSelected.item.length; j++){
                    if (presenter.currentSelected.item[j] === true) {
                        presenter.markCorrectAnswerAsSelected(j);
                    }

                    if (presenter.currentSelected.item[j] === false &amp;&amp; k != number - Counter){
                        presenter.markCorrectAnswerAsSelected(j);
                        k++;
                    }
                }
            } else {
                for (let j = 1; j &lt; presenter.currentSelected.item.length; j++){
                    if (presenter.currentSelected.item[j] === true) {
                        if (k != number){
                            presenter.markCorrectAnswerAsSelected(j);
                            k++;
                        }
                    }
                }
            }
        }
        presenter.addShowAnswersClass();
    }

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive
            &amp;&amp; !presenter.isGradualShowAnswersActive) {
            return;
        }
        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();

        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
        presenter.removeShowAnswersClass();
        presenter.clear();

        for (let j = 1; j &lt; presenter.currentSelected.item.length; j++){
            if (presenter.currentSelected.item[j] === true) {
                presenter.markCorrectAnswerAsSelected(j);
            }
        }
        return true;
    };

    presenter.hideElementsSA = function(item){
        presenter.hideAnswers();
    };

    presenter.gradualHideAnswers = function() {
        presenter.hideAnswers();
    };

    presenter.keyboardController = function(keycode, isShiftDown, event) {
        event.preventDefault();

        const keys = {
            ENTER: 13,
            ESCAPE: 27,
            SPACE: 32,
            ARROW_LEFT: 37,
            ARROW_UP: 38,
            ARROW_RIGHT: 39,
            ARROW_DOWN: 40,
            TAB: 9
        };

        switch (keycode) {
            case keys.ENTER:
                presenter.enter(event);
                break;
            case keys.ESCAPE:
                presenter.escape();
                break;
            case keys.SPACE:
                presenter.select();
                break;
            case keys.ARROW_LEFT:
                presenter.leftArrowHandler();
                break;
            case keys.ARROW_UP:
                presenter.previousRow();
                break;
            case keys.ARROW_RIGHT:
                presenter.rightArrowHandler();
                break;
            case keys.ARROW_DOWN:
                presenter.nextRow();
                break;
            case keys.TAB:
                presenter.tabHandler(isShiftDown);
                break;
        }
    };

    presenter.tabHandler = function (isShiftDown) {
        if (isShiftDown) {
            presenter.previousElement();
        } else {
            presenter.nextElement();
        }
    }

    presenter.leftArrowHandler = function () {
        if (presenter.isSquareSelected() || presenter.isCircleSelected()) {
            presenter.nextElement();
        } else {
            presenter.previousElement();
        }
    }

    presenter.rightArrowHandler = function () {
        if (presenter.isSquareSelected() || presenter.isCircleSelected()) {
            presenter.previousElement();
        } else {
            presenter.nextElement();
        }
    }

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new FractionsKeyboardController(
                presenter.$view,
                presenter.getNumberOfElements()
        );
    };

    presenter.getNumberOfElements = function () {
        switch (presenter.configuration.figure) {
            case presenter.FIGURES.CIRCLE:
                return presenter.configuration.circleParts;

            case presenter.FIGURES.RECTANGULAR:
                return presenter.configuration.rectHorizontal * presenter.configuration.rectVertical;

            case presenter.FIGURES.SQUARE:
                return presenter.configuration.squareParts;
        }
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    function FractionsKeyboardController (elements, elementsCount) {
        KeyboardController.call(this, elements, elementsCount);
    }

    FractionsKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    FractionsKeyboardController.prototype.constructor = FractionsKeyboardController;

    presenter.exitWCAGMode = function () {
        presenter.setWCAGStatus(false);
        presenter.removeMarkedElement();
        presenter.isFirstEnter = true;
    }

    presenter.select = function () {
        const element = $(`#${presenter.configuration.addonId}${presenter.markedItemIndex}`);

        presenter.markElementAsClicked(element.get(0));

        presenter.readOnSelect();
    }

    presenter.enter = function (event) {
        if (event.ctrlKey || event.shiftKey) {
            presenter.exitWCAGMode();
            return;
        }

        if (presenter.isFirstEnter) {
            presenter.markedItemIndex = 1;
            presenter.orderItemIndex = 1;
            presenter.markItem(1);
        }

        presenter.readOnEnter();
        presenter.isFirstEnter = false;
    }

    presenter.nextElement = function () {
        presenter.increaseIndex();
        presenter.markItem(presenter.markedItemIndex);

        presenter.readMarkedElement();
    };

    presenter.previousElement = function () {
        presenter.decreaseIndex();
        presenter.markItem(presenter.markedItemIndex);

        presenter.readMarkedElement();
    };

    presenter.previousRow = function () {
        if (presenter.isSquareSelected() || presenter.isCircleSelected()) {
            presenter.increaseIndex();
            presenter.markItem(presenter.markedItemIndex);
        } else {
            const offset = presenter.markedItemIndex - presenter.configuration.rectHorizontal &lt; 1 ? presenter.markedItemIndex : presenter.markedItemIndex - presenter.configuration.rectHorizontal;
            presenter.markedItemIndex = offset;
            presenter.orderItemIndex = offset;
            presenter.markItem(presenter.markedItemIndex);
        }

        presenter.readMarkedElement();
    };

    presenter.nextRow = function () {
        if (presenter.isSquareSelected() || presenter.isCircleSelected()) {
            presenter.decreaseIndex();
            presenter.markItem(presenter.markedItemIndex);
        } else {
            const totalElements = presenter.configuration.rectHorizontal * presenter.configuration.rectVertical
            const offset = presenter.markedItemIndex + presenter.configuration.rectHorizontal &gt; totalElements ? presenter.markedItemIndex : presenter.markedItemIndex + presenter.configuration.rectHorizontal;
            presenter.markedItemIndex = offset;
            presenter.orderItemIndex = offset;
            presenter.markItem(presenter.markedItemIndex);
        }

        presenter.readMarkedElement();
    };

    presenter.escape = function () {
        presenter.removeMarkedElement();
        presenter.isFirstEnter = true;
    }

    presenter.isSquareSelected = function () {
        return presenter.configuration.figure === presenter.FIGURES.SQUARE;
    }

    presenter.isCircleSelected = function () {
        return presenter.configuration.figure === presenter.FIGURES.CIRCLE;
    }

    presenter.increaseIndex = function () {
        presenter.orderItemIndex += 1;
        presenter.updateMarkedItemIndex();
        if (presenter.isSquareSelected()) {
            presenter.markedItemIndex = getIndexesInOrder(presenter.configuration.squareParts)[presenter.orderItemIndex - 1];
        } else {
            presenter.markedItemIndex = presenter.orderItemIndex;
        }
    }

    presenter.decreaseIndex = function () {
        presenter.orderItemIndex -= 1;
        presenter.updateMarkedItemIndex();
        if (presenter.isSquareSelected()) {
            presenter.markedItemIndex = getIndexesInOrder(presenter.configuration.squareParts)[presenter.orderItemIndex - 1];
        } else {
            presenter.markedItemIndex = presenter.orderItemIndex;
        }
    }

    presenter.markItem = function (itemIndex) {
        const dItemValue = $(`#${presenter.configuration.addonId}${itemIndex}`).attr('d')
        presenter.removeMarkedElement();

        const newElement = document.createElementNS("http://www.w3.org/2000/svg", 'path'); //Create a path in SVG's namespace
        newElement.setAttribute("d",dItemValue);
        newElement.setAttribute('id', 'Fractions-keyboard-nav')
        newElement.style.stroke = presenter.markedItemColor;
        newElement.style.strokeWidth = presenter.configuration.strokeWidth;
        newElement.style.strokeDasharray = presenter.markedItemBorderData;
        newElement.style.fill = 'transparent';
        newElement.style.strokeLinejoin = 'round';
        newElement.classList.add(presenter.configuration.addonId);

        const $addonElement = $(`#${presenter.configuration.addonId}`);
        const svg = $addonElement.find('.FractionsWrapper').get(0).children[0];
        svg.appendChild(newElement);
    }

    presenter.removeMarkedElement = function () {
        const element = document.getElementById('Fractions-keyboard-nav');
        if (element?.parentNode) {
            element.parentNode.removeChild(element);
        }
    }

    presenter.updateMarkedItemIndex = function () {
        const elementsCount = presenter.getNumberOfElements();
        if (presenter.orderItemIndex &gt; elementsCount) {
            presenter.orderItemIndex = 1;
        } else if (presenter.orderItemIndex &lt; 1) {
            presenter.orderItemIndex = elementsCount;
        }
    }

    presenter.isMarkedElementSelected = function () {
        const element$ = $(`#${presenter.configuration.addonId}${presenter.markedItemIndex}`);

        return element$.hasClass('selected');
    }

    presenter.getTextToSpeechOrNull = function () {
        if (presenter.playerController) {
            return presenter.playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function (data) {
        const tts = presenter.getTextToSpeechOrNull();

        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            Selected: presenter.DEFAULT_TTS_PHRASES.SELECTED,
            Deselected: presenter.DEFAULT_TTS_PHRASES.DESELECTED,
            Item: presenter.DEFAULT_TTS_PHRASES.ITEM,
            Correct: presenter.DEFAULT_TTS_PHRASES.CORRECT,
            Wrong: presenter.DEFAULT_TTS_PHRASES.WRONG,
            of: presenter.DEFAULT_TTS_PHRASES.OF,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.Selected),
            Deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.Deselected),
            Item: TTSUtils.getSpeechTextProperty(
                speechTexts.Item.Item,
                presenter.speechTexts.Item),
            Correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.Correct),
            Wrong: TTSUtils.getSpeechTextProperty(
                speechTexts.Wrong.Wrong,
                presenter.speechTexts.Wrong),
            of: TTSUtils.getSpeechTextProperty(
                speechTexts.of.of,
                presenter.speechTexts.of),
        };
    };
    
    presenter.readOnEnter = function () {
        const textVoiceObject = [];
        const numberOfSelectedItems = presenter.getNumberOfSelectedItems().toString();
        const numberOfAllItems = (presenter.currentSelected.item.length - 1).toString();
        let text = [presenter.speechTexts.Selected, numberOfSelectedItems, presenter.speechTexts.of, numberOfAllItems].join(' ');

        if (presenter.isErrorCheckingMode) {
            text += ` ${presenter.getSelectionMark(+numberOfSelectedItems)}`;
        }
        pushMessageToTextVoiceObject(textVoiceObject, text);

        if (presenter.isFirstEnter &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.isShowAnswersActive) {
            const option = presenter.isMarkedElementSelected() ? presenter.speechTexts.Selected : presenter.speechTexts.Deselected;
            const selectionText = [presenter.speechTexts.Item, "1", option].join(' ');
            pushMessageToTextVoiceObject(textVoiceObject, selectionText);
        }

        presenter.speak(textVoiceObject);
    }

    presenter.getNumberOfSelectedItems = function () {
        if (presenter.isShowAnswersActive) {
            return presenter.configuration.correctAnswer;
        }
        return presenter.currentSelected.item.filter(_item =&gt; _item === true).length;
    }

    presenter.getSelectionMark = function (userSelectedItems) {
        if (userSelectedItems === presenter.configuration.correctAnswer) {
            return presenter.speechTexts.Correct;
        }
        return presenter.speechTexts.Wrong;
    }

    presenter.readMarkedElement = function () {
        const textVoiceObject = [];
        const option = presenter.isMarkedElementSelected() ? presenter.speechTexts.Selected : presenter.speechTexts.Deselected;
        const text = [presenter.speechTexts.Item, presenter.orderItemIndex, option].join(' ');
        pushMessageToTextVoiceObject(textVoiceObject, text);

        presenter.speak(textVoiceObject);
    }

    presenter.readOnSelect = function () {
        const textVoiceObject = [];
        const option = presenter.isMarkedElementSelected() ? presenter.speechTexts.Selected : presenter.speechTexts.Deselected;
        pushMessageToTextVoiceObject(textVoiceObject, option);

        presenter.speak(textVoiceObject);
    }

    function pushMessageToTextVoiceObject(textVoiceObject, message) {
        textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    var SquareShapeElement = function (width, height, x, y, currentCutIndex, id) {
        this.width = Math.min(width, height);
        this.height = Math.min(width, height);
        this.x = x;
        this.y = y;
        this.currentCutIndex = currentCutIndex;
        this.id = id;
    };

    SquareShapeElement.prototype = {
        calculateValues: function (maxWidth, maxHeight) {
            var stroke = presenter.configuration.strokeWidth;
            var originY = this.y + this.height;
            if (this.x == 0) {
                this.x += stroke;
                this.width -= stroke;
            } else {
                this.width -= stroke;
            }

            if (this.y == 0) {
                this.y += stroke;
                this.height -= stroke;
            }
            if (originY == maxWidth) {
                this.height -= stroke;
            }
        },

        cutToHalf: function (maxWidth, maxHeight) {
            if (this.currentCutIndex == 1) {
                return this.cutToVerticallyHalf();
            } else if (this.currentCutIndex == 2) {
                return this.cutToHorizontallyHalf();
            } else {
                return this.cutToTriangleHalf(maxWidth, maxHeight);
            }
        },

        cutToVerticallyHalf: function () {
            var elements = [];
            var newWidth = this.width / 2;
            var newHeight = this.height;
            elements.push(new RectangleShapeElement(newWidth, newHeight, this.x, this.y, this.currentCutIndex + 1, this.id));
            elements.push(new RectangleShapeElement(newWidth, newHeight, this.x + newWidth, this.y, this.currentCutIndex + 1, this.id));

            return elements;
        },

        cutToHorizontallyHalf: function () {
            var elements = [];
            var newWidth = this.width;
            var newHeight = this.height / 2;
            elements.push(new RectangleShapeElement(newWidth, newHeight, this.x, this.y, this.currentCutIndex + 1, this.id));
            elements.push(new RectangleShapeElement(newWidth, newHeight, this.x, this.y + newHeight, this.currentCutIndex + 1, this.id));

            return elements;
        },

        cutToTriangleHalf: function (maxWidth, maxHeight) {
            var size = Math.min(maxWidth, maxHeight);
            var xCenter = size / 2;
            var yCenter = size / 2;

            var x1 = this.x;
            var y1 = this.y;

            if (x1 != 0) {
                x1 = size;
            }

            if (y1 != 0) {
                y1 = size;
            }


            var elements = [];
            elements.push(new TriangleShapeElement(this.width, this.height, x1,y1, xCenter, y1, xCenter, yCenter, this.currentCutIndex + 1, this.id));
            elements.push(new TriangleShapeElement(this.width, this.height, x1,y1, x1, yCenter, xCenter, yCenter, this.currentCutIndex + 1, this.id));
            return elements;
        },

        getSVGString: function (id) {
            var width = this.width;
            var height = this.height;
            return '&lt;path ' +
                'id="' + this.id + id + '" ' +
                'class="' + this.id + '" ' +
                'd="M' + (this.x) + ',' + (this.y) + 'h' + (width) + ' v' + (height) + ' h' + (-width) + ' v' + (-height) + '" ' +
                'stroke-width="' + presenter.configuration.strokeWidth + '" ' +
                'style="stroke: ' + presenter.configuration.strokeColor + '; ' +
                'fill: ' + presenter.configuration.emptyColor + ';" /&gt;';
        }

    };

    var RectangleShapeElement = function (width, height, x, y, currentCutIndex, id) {
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
        this.currentCutIndex = currentCutIndex;
        this.id = id;
    };
    RectangleShapeElement.prototype = Object.create(SquareShapeElement.prototype);

    var TriangleShapeElement = function (width, height, x1, y1, x2, y2, x3, y3, currentCutIndex, id) {
        this.width = width;
        this.height = height;
        this.x1 = x1;
        this.y1 = y1;

        this.x2 = x2;
        this.y2 = y2;

        this.x3 = x3;
        this.y3 = y3;
        this.currentCutIndex = currentCutIndex;
        this.id = id;
    };
    TriangleShapeElement.prototype = Object.create(SquareShapeElement.prototype);

    TriangleShapeElement.prototype.getSVGString = function (id) {
        return '&lt;path ' +
            'id="' + this.id + id + '" ' +
            'class="' + this.id + '" ' +
            'd="M' + this.x1 + ',' + this.y1 + ',' + this.x2 + ',' + this.y2 + ',' + this.x3 + ',' + this.y3 + 'Z" ' +
            'stroke-width="' + presenter.configuration.strokeWidth + '" ' +
            'style="stroke: ' + presenter.configuration.strokeColor + '; ' +
            'fill: ' + presenter.configuration.emptyColor + ';" /&gt;';
    };

    TriangleShapeElement.prototype.calculateValues = function(maxWidth, maxHeight) {
        var stroke = presenter.configuration.strokeWidth;
        if (this.x1 == 0) {
            this.x1 += stroke / 2;
        }

        if (this.x1 == maxWidth) {
            this.x1 -= stroke / 2;
        }

        if (this.x2 == 0) {
            this.x2 += stroke / 2;
        }

        if (this.x2 == maxWidth) {
            this.x2 -= stroke / 2;
        }

        if (this.y1 == 0) {
            this.y1 += stroke / 2;
        }

        if (this.y2 == 0) {
            this.y2 += stroke / 2;
        }

        if (this.y1 == maxWidth) {
            this.y1 -= stroke / 2;
        }

        if (this.y2 == maxWidth) {
            this.y2 -= stroke / 2;
        }
    };

    TriangleShapeElement.prototype.cutToHalf = function (maxWidth, maxHeight) {
        var elements = [];
        var maxSize = Math.min(maxWidth, maxHeight);
        if ((this.x1 == 0 &amp;&amp; this.x2 == 0) || (this.x1 == maxSize &amp;&amp; this.x2 == maxSize)) {
            elements.push(new TriangleShapeElement(this.width, this.height / 2, this.x1, this.y1, this.x2, (this.y1 + this.y2) / 2, this.x3, this.y3, this.currentCutIndex + 1, this.id));
            elements.push(new TriangleShapeElement(this.width, this.height / 2, this.x1, (this.y1 + this.y2) / 2, this.x2, this.y2, this.x3, this.y3, this.currentCutIndex + 1, this.id));
        } else {
            elements.push(new TriangleShapeElement(this.width / 2, this.height, this.x1, this.y1, (this.x2 + this.x1) / 2, this.y2, this.x3, this.y3, this.currentCutIndex + 1, this.id));
            elements.push(new TriangleShapeElement(this.width / 2, this.height, (this.x2 + this.x1) / 2, this.y2, this.x2, this.y2, this.x3, this.y3, this.currentCutIndex + 1, this.id));
        }
        return elements;
    };

    function getIndexesInOrder(numberOfElements) {
        switch (numberOfElements) {
            case 1:
                return [1];
            case 4:
                return [1, 2, 4, 3];
            case 8:
                return [1, 2, 4, 3, 7, 8, 6, 5];
            default:
                return calculateIndexesInOrder(numberOfElements);
        }
    }

    function calculateIndexesInOrder(numberOfElements) {
        const offset = (numberOfElements / 8) // 8 come from 4 side each divided on 2

        return [1,
            ...getIncrementIndexes(1+offset, offset),
            ...getDecrementIndexes(4*offset, offset),
            ...getIncrementIndexes(1+2*offset, offset),
            ...getDecrementIndexes(7*offset, offset),
            ...getIncrementIndexes(1+7*offset, offset),
            ...getDecrementIndexes(6*offset, offset),
            ...getIncrementIndexes(1+4*offset, offset),
            ...getDecrementIndexes(offset, offset-1),
        ]
    }

    function getIncrementIndexes(startIndex, offset) {
        return Array.from({length: offset}, (_, i) =&gt; startIndex + i);
    }

    function getDecrementIndexes(endIndex, offset) {
        return Array.from({length: offset}, (_, i) =&gt; endIndex - i);
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="FractionsBinder" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Fractions" name="Addons" nameLabel="FractionsBinder_property_addons" type="text"/>
        <property displayName="Correct Elements" name="CorrectElements" nameLabel="FractionsBinder_property_correct_elements" type="string"/>
    </model>
<css/><view/><preview>&lt;p&gt;FractionBinder&lt;/p&gt;
</preview><presenter>function AddonFractionsBinder_create(){
			
        var presenter = function () {};

    presenter.configuration = {};
    presenter.loadFirstTime = true;
    presenter.isPreview = false;

    function presenterLogic () {
        if(presenter.isPreview) return;
        presenter.addons = (presenter.model.Addons).split('\n');
        presenter.$view.css('visible', 'hidden');
        presenter.CorrectElements = presenter.model.CorrectElements;
        presenter.check = {};
        presenter.initialMarks = 0;

        for (var i = 0; i &lt; presenter.addons.length; i++) {

                    if(presenter.getFraction(presenter.addons[i]) != null &amp;&amp; presenter.getFraction(presenter.addons[i]).allElements != undefined){
                        presenter.check[i] = true;
                        //presenter.initialMarks = presenter.initialMarks + presenter.getFraction(presenter.addons[i]).getInitialMarks();
                        presenter.initialMarks = presenter.initialMarks + presenter.getFraction(presenter.addons[i]).getCurrentNumber();
                    } else{
                        presenter.check[i] = false;
                   }
            }

       presenter.loadFirstTime = false;
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('ValueChanged', this);
    };

    presenter.getFraction = function (moduleID) {
        return presenter.playerController.getModule(moduleID);
    };

    presenter.run = function (view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.isPreview = false;
        //if(presenter.loadFirstTime) presenterLogic();

            presenter.eventBus.addEventListener('ShowAnswers', this);
            presenter.eventBus.addEventListener('HideAnswers', this);
            presenter.eventBus.addEventListener('PageLoaded', this);
            //presenter.eventBus.addEventListener('AddonFractionsBinder', this);

        //presenter.playerController.getEventBus().sendEvent('AddonFractionsBinder', {view,model,false});
    };


    presenter.triggerFrameChangeEvent = function(view,model,isPreview) {
        presenter.eventBus.sendEvent('AddonFractionsBinder', '');

    };


    presenter.createPreview = function (view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.isPreview = true;

        if(presenter.loadFirstTime) presenterLogic();
    };

    presenter.getState = function () {
        var addons = presenter.addons;
        var check = presenter.check;
        var loadFirstTime = presenter.loadFirstTime;
        var initialMarks = presenter.initialMarks;
        var correct = presenter.CorrectElements;
        return JSON.stringify({
            correct: correct,
            check: check,
            addons: addons,
            loadFirstTime : loadFirstTime,
            initialMarks : initialMarks
        });
    };

    presenter.setState = function (state) {

        var parsedState = JSON.parse(state);
        presenter.check = parsedState.check;
        presenter.addons = parsedState.addons;
        presenter.initialMarks = parsedState.initialMarks;
        presenter.loadFirstTime = parsedState.loadFirstTime;
        presenter.CorrectElements = parsedState.correct;
        /*
        for (var i = 0; i &lt; presenter.addons.length; i++) {

                    if(presenter.check[i]){
                        presenter.getFraction(presenter.addons[i]).markAsEmpty();
                    }
            }
            */
    };

    presenter.getMaxScore = function () {
        //if(presenter.loadFirstTime) presenterLogic();
        if(presenter.initialMarks == presenter.CorrectElements) {
            return 0;
        } else {
            return 1;
        }
    };


    presenter.getScore = function () {
        //if(presenter.loadFirstTime) presenterLogic();

            if(presenter.initialMarks == presenter.CorrectElements) {
                return 0;
            } else{
                return presenter.currentMarks() == presenter.CorrectElements ? 1 : 0;
            }
        //} else {
          //  return 0;
        //}
    };

    presenter.getErrorCount = function () {
        //if(!presenter.loadFirstTime){
            if(presenter.initialMarks == presenter.CorrectElements &amp;&amp; presenter.currentMarks() != presenter.CorrectElements) {
                return 1;
            }

            if(presenter.initialMarks == presenter.currentMarks()) {
                return 0;

            } else {
                return presenter.getMaxScore() - presenter.getScore();

            }
        //} else{
        //    return 0;
        //}
    };


    presenter.setShowErrorsMode = function () {
        //if(presenter.loadFirstTime) presenterLogic();
         //fractions markAsCorrect || markAsWrong
        var current = presenter.currentMarks();
            if(presenter.initialMarks != current){
                 if(current == presenter.CorrectElements){

                        for (var i = 0; i &lt; presenter.addons.length; i++) {

                            if(presenter.check[i]){
                                presenter.getFraction(presenter.addons[i]).markAsCorrect();
                                //presenter.getFraction(presenter.addons[i]).isErrorCheckMode(true);
                            }
                        }

                 } else{
                        for (var i = 0; i &lt; presenter.addons.length; i++) {

                            if(presenter.check[i]){
                                presenter.getFraction(presenter.addons[i]).markAsWrong();
                               //presenter.getFraction(presenter.addons[i]).isErrorCheckMode(true);
                            }
                        }
                 }


            }


    };

    presenter.currentMarks = function(){
        var marks = 0;
        for (var i = 0; i &lt; presenter.addons.length; i++) {

                if(presenter.check[i]){
                    marks = marks + presenter.getFraction(presenter.addons[i]).getCurrentNumberSA();
                }
        }
        return marks;
    };

    presenter.setWorkMode = function () {

            for (var i = 0; i &lt; presenter.addons.length; i++) {

                    if(presenter.check[i]){
                        presenter.getFraction(presenter.addons[i]).markAsEmpty();
                        presenter.getFraction(presenter.addons[i]).isErrorCheckMode(false);
                    }
            }

    };

    presenter.showAnswers = function () {
        //if(presenter.loadFirstTime) presenterLogic();
        var elementsLeft = presenter.CorrectElements;
        var elements = 0;
        var showElements = 0;
            for (var i = 0; i &lt; presenter.addons.length; i++) {

                    if(presenter.check[i]){
                        if(elementsLeft &gt; 0){
                            elements = presenter.getFraction(presenter.addons[i]).allElements();
                            showElements = elementsLeft - elements &gt; 0 ? elements : elementsLeft;
                            elementsLeft = elementsLeft - elements &gt; 0 ? elementsLeft - elements : 0;
                        } else {
                            showElements = 0;
                        }
                    //console.log(elementsLeft);
                    //console.log(showElements);
                        presenter.getFraction(presenter.addons[i]).markAsEmpty();

                            presenter.getFraction(presenter.addons[i]).showElementsSA(showElements);
                                //presenter.getFraction(presenter.addons[i]).isErrorCheckMode(true);

                        //presenter.getFraction(presenter.addons[i]).addShowAnswersClass();
                    }
            }

        //presenter.CorrectElements

        //fractions showElementsSA(ilosc_elementow)
    };

    presenter.hideAnswers = function () {

        for (var i = 0; i &lt; presenter.addons.length; i++) {

                    if(presenter.check[i]){
                        presenter.getFraction(presenter.addons[i]).hideElementsSA();
                        //presenter.getFraction(presenter.addons[i]).isErrorCheckMode(false);
                        //presenter.getFraction(presenter.addons[i]).removeShowAnswersClass();
                    }
        }

    };

    presenter.reset = function () {

        //fractions markAsEmpty

    };

    presenter.onEventReceived = function (eventName, eventData) {

        if (eventName == "PageLoaded"){
            if(presenter.loadFirstTime) presenterLogic();
        }

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }


    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Gap_Binder" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property name="Items" nameLabel="Gap_Binder_property_items" type="list">
            <property name="Modules" nameLabel="Gap_Binder_property_modules" type="string"/>
            <property isLocalized="true" name="Answers" nameLabel="Gap_Binder_property_answers" type="text"/>
        </property>
    </model>
<css/><view>&lt;p&gt;&lt;/p&gt;
</view><preview>&lt;p&gt;Addon Gap_Binder&lt;/p&gt;
</preview><presenter>function AddonGap_Binder_create() {

    var presenter = function () { }

    // addon's parsed configuration data
    presenter.modulesIDs = [];
    presenter.answers = [];

    // addon's modes
    presenter.isErrorMode = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.GSACounter = 0;

    const SUPPORTED_MODULES_TYPES = ["Text", "table"];

    let scoreData = {
        emptyElementsNumber: 0,
        correctElementsNumber: 0,
        maxCorrectElementsNumber: 0,
        usedAnswersIndex: [],
        gapsIDsWithCorrectAnswer: [],
    }
    let savedGapsValues = [];

    presenter.validatedPages = [];

    presenter.ERROR_CODES = {
        INVALID_ID: "Provided module ID is incorrect.",
        INVALID_NUMBER_GAP: "The number of answers provided is incorrect.",
        INVALID_NUMBER_ITEMS: "Addon supports only one item in the configuration.",
        INVALID_NULL: "Value cannot be null."
    };

    presenter.CSS_CLASSES = {
        INVALID_CONFIGURATION: "gap_binder_invalid",
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);

        setTimeout(() =&gt; {
            presenter.configuration = presenter.validateModel(model);
            $(view).removeClass(presenter.CSS_CLASSES.INVALID_CONFIGURATION);
            if (presenter.configuration.isError) {
                DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
                $(view).addClass(presenter.CSS_CLASSES.INVALID_CONFIGURATION);
            }
        }, 1250);
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);

        MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.view = view;
        presenter.$view = $(view);
        presenter.readModelItems(model.Items);
    };

    presenter.getActivitiesCount = function () {
        return presenter.getGapsNumber();
    }

    presenter.getGapsNumber = function () {
        let gapsCounter = 0;
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            gapsCounter += moduleGaps.length ? moduleGaps.length : 0;
        });
        return gapsCounter;
    }

    presenter.readModelItems = function (items) {
        setModulesIDs(items);
        setAnswers(items);
    };

    function setModulesIDs(items) {
        presenter.modulesIDs.length = 0;
        let newModules = [];

        items.forEach(item =&gt; {
            const modulesIDs = item.Modules.split(",");
            modulesIDs.forEach(moduleID =&gt; {
                moduleID = moduleID.trim();
                newModules.push(moduleID);
            })
        });

        presenter.modulesIDs = [...new Set(newModules)];
    }

    function setAnswers(items) {
        presenter.answers.length = 0;

        items.forEach(item =&gt; {
            let answers = item.Answers.split('\n');
            presenter.answers.push(...answers);
        });
    }

    presenter.validateModel = function (model) {
        const oneItemInConfiguration = isOneItemInConfiguration(model);
        if (!oneItemInConfiguration) {
            return { isError: true, errorCode: 'INVALID_NUMBER_ITEMS' };
        }

        const someItemEmpty = isSomeItemFieldEmpty(model);
        if (someItemEmpty) {
            return { isError: true, errorCode: 'INVALID_NULL' };
        }

        const eachModuleOnCurrentPage = isEachModuleOnCurrentPage()
        if (!eachModuleOnCurrentPage) {
            return { isError: true, errorCode: 'INVALID_ID' };
        }

        const numberOfProvidedAnswersCorrect = isNumberOfProvidedAnswersCorrect();
        if (!numberOfProvidedAnswersCorrect) {
            return { isError: true, errorCode: 'INVALID_NUMBER_GAP' };
        }

        return {
            isError: false,
        }
    };

    function isOneItemInConfiguration(model) {
        return model.Items.length === 1;
    }

    function isSomeItemFieldEmpty(model) {
        return model.Items.some(item =&gt; {
            return ((!item.Modules || item.Modules.trim() == "")
                || !item.Answers || item.Answers.trim() == "");
        });
    }

    function isEachModuleOnCurrentPage() {
        const $pageGaps = findCurrentPageGaps();
        const pageModulesIDs = getGapsModulesIDs($pageGaps);

        let foundModulesNumber = 0;
        presenter.modulesIDs.forEach(configurationModuleID =&gt; {
            if (pageModulesIDs.includes(configurationModuleID)) {
                foundModulesNumber++;
            }
        });

        return foundModulesNumber === presenter.modulesIDs.length;
    }

    function getGapsModulesIDs($gaps) {
        let modulesIDs = [];

        for (let i = 0; i &lt; $gaps.length; i++) {
            let parent = $gaps[i].closest('.ice_module');
            if (parent == null) {
                parent = $gaps[i].closest('.ic_text');
            }

            if (parent == null) {
                parent = $gaps[i].closest('.addon_Table');
            }

            if (parent != null &amp;&amp; $(parent).attr('id').length &gt; 0) {
                modulesIDs.push($(parent).attr('id'));
            }
        }
        return [...new Set(modulesIDs)];
    }

    function isNumberOfProvidedAnswersCorrect() {
        let count = 0;
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const $element = $('body').find(`#${moduleID}`);
            const $elementGaps = findGaps($element);
            const elementGaps = $elementGaps.toArray();
            count += elementGaps.length;
        });

        return count === presenter.answers.length;
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.currentPageIndex = controller.getCurrentPageIndex();
    };

    presenter.setEventBus = function(eventBus) {
        const eventsName = ['ValueChanged', 'PageLoaded', 'ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        presenter.eventBus = eventBus;

        eventsName.forEach(eventName =&gt; {
            presenter.eventBus.addEventListener(eventName, this);
        });
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case 'ShowAnswers':
                presenter.showAnswers();
                break;

            case 'HideAnswers':
                presenter.hideAnswers();
                break;

            case 'ValueChanged':
                if (!presenter.isShowAnswersActive) handleValueChangedEvent(eventData);
                break;

            case 'GradualShowAnswers':
                presenter.gradualShowAnswers(eventData);
                break;

            case 'GradualHideAnswers':
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.showAnswers = () =&gt; {
        if (presenter.isShowAnswersActive) {
            return;
        }

        if (presenter.isErrorMode) {
            handleWorkMode();
        }

        checkCorrectnessOfAnswers();
        handleShowAnswers();
    };

    function handleShowAnswers() {
        presenter.isShowAnswersActive = true;

        savedGapsValues.length = 0;
        let answerIndex = 0;
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            const module = getModule(moduleID);

            for (let gapIndex = 0; gapIndex &lt; moduleGaps.length; gapIndex++) {
                const gap = moduleGaps[gapIndex];
                saveNextGapValue(gap);
                loadCorrectAnswer(gap, answerIndex);
                answerIndex++;

                module.disableGap(gapIndex + 1);
            }
        });
    }

    presenter.hideAnswers = () =&gt; {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        if (presenter.isErrorMode) {
            handleWorkMode();
        }

        handleHideAnswers();
    };

    function handleHideAnswers() {
        presenter.isShowAnswersActive = false;

        let answerIndex = 0;
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            const module = getModule(moduleID);

            for (let gapIndex = 0; gapIndex &lt; moduleGaps.length; gapIndex++) {
                const gap = moduleGaps[gapIndex];
                presenter.removeShowAnswersClass(gap);
                loadSavedGapValue(gap, answerIndex);
                answerIndex++;

                module.enableGap(gapIndex + 1);
            }
        });
    }

    presenter.gradualShowAnswers = function (eventData) {
        if (eventData.moduleID !== presenter.addonID) {
            return;
        }

        if (presenter.isErrorMode) {
            handleWorkMode();
        }

        if (!presenter.isGradualShowAnswersActive) {
            checkCorrectnessOfAnswers();
            presenter.isGradualShowAnswersActive = true;
        }

        let gradualAnswerIndex = -1;

        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            const module = getModule(moduleID);

            for (let gapIndex = 0; gapIndex &lt; moduleGaps.length; gapIndex++) {
                gradualAnswerIndex += 1;
                if (gradualAnswerIndex === presenter.GSACounter) {
                    const gap = moduleGaps[gapIndex];
                    saveNextGapValue(gap);
                    loadCorrectAnswer(gap, gradualAnswerIndex);
                    module.disableGap(gapIndex + 1);
                }
            }
        });
        presenter.updateGSACounter(eventData);
    }

    presenter.gradualHideAnswers = function () {
        if (!presenter.isGradualShowAnswersActive) return;

        presenter.isGradualShowAnswersActive = false;
        presenter.GSACounter = 0;
        handleHideAnswers();
    }

    presenter.updateGSACounter = function (eventData) {
        let itemIndex = parseInt(eventData.item, 10)
        while (itemIndex &lt; presenter.GSAcounter) {
            itemIndex++;
        }
        presenter.GSACounter = itemIndex + 1;
    }

    function saveNextGapValue(gap) {
        const valueToSave = {
            innerHTML: gap.innerHTML,
            value: gap.value,
            id: gap.id,
        };
        savedGapsValues.push(valueToSave);
    }

    function loadCorrectAnswer(gap, index) {
        const answer = presenter.answers[index];
        gap.innerHTML = answer;
        gap.value = answer;
        presenter.addShowAnswersClass(gap);
    }

    presenter.addShowAnswersClass = function(gap) {
        let isTable = $(gap).parents('.addon_Table').length &gt; 0;
        let isFilledGap = gap.classList.contains('ic_filled_gap');

        if (isTable) {
            gap.classList.add('ic_gap-show-answers');
        } else {
            if (isFilledGap) {
                gap.classList.add('ic_filled_gap-correct-answer');
            } else {
                gap.classList.add('ic_gap-correct-answer');
            }
        }

    }

    presenter.removeShowAnswersClass = function(gap) {
        gap.classList.remove('ic_gap-correct-answer');
        gap.classList.remove('ic_filled_gap-correct-answer');
        gap.classList.remove('ic_gap-show-answers');
    }

    function loadSavedGapValue(gap, index) {
        if (index &gt;= savedGapsValues.length) {
            return;
        }
        const valueToLoad = savedGapsValues[index];
        gap.innerHTML = valueToLoad.innerHTML;
        gap.value = valueToLoad.value;
    }

    presenter.setShowErrorsMode = function () {
        if (presenter.isErrorMode) {
            return;
        }

        handleShownAnswers();

        handleShowErrorsMode();
    };

    function handleShownAnswers() {
        if (presenter.isShowAnswersActive) {
            handleHideAnswers();
        }

        if (presenter.isGradualShowAnswersActive) {
            presenter.isGradualShowAnswersActive = false;
            handleHideAnswers();
        }
    }

    function handleShowErrorsMode() {
        presenter.isErrorMode = true;

        presenter.validatedPages[presenter.currentPageIndex] = true;

        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            const module = getModule(moduleID);

            for (let gapIndex = 0; gapIndex &lt; moduleGaps.length; gapIndex++) {
                const gapIndexInModule = gapIndex + 1;
                const gap = moduleGaps[gapIndex];

                if (isCorrectAnswerInGap(gap.id)) {
                    module.markGapAsCorrect(gapIndexInModule);
                } else if (!isGapEmpty(gap)) {
                    module.markGapAsWrong(gapIndexInModule);
                } else {
                    module.markGapAsEmpty(gapIndexInModule);
                }
            }
        })
    }

    function isGapEmpty(gap) {
        return ((!gap.innerHTML &amp;&amp; !gap.value)
            || (gap.innerHTML === "&amp;nbsp;" &amp;&amp; !gap.value)
            || (gap.innerHTML === "" &amp;&amp; !gap.value)
        );
    }

    presenter.reset = function () {
        resetScoreData();
        presenter.hideAnswers();
        presenter.gradualHideAnswers();
        presenter.setWorkMode();
        presenter.GSACounter = 0;
    };

    presenter.setWorkMode = function () {
        if (!presenter.isErrorMode) {
            return;
        }

        handleShownAnswers();

        handleWorkMode();
    };

    function handleWorkMode() {
        presenter.isErrorMode = false;
        enableAllGapsOfConnectedModules();
    }

    function enableAllGapsOfConnectedModules() {
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const module = getModule(moduleID);
            module.enableAllGaps();
        });
    }

    presenter.getErrorCount = function () {
        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return getErrorCountFromScoreData();
        }

        handleShownAnswers();
        checkCorrectnessOfAnswers();
        if (scoreData.correctElementsNumber === scoreData.maxCorrectElementsNumber) {
            return 0;
        }
        return getErrorCountFromScoreData();
    };

    function getErrorCountFromScoreData() {
        return scoreData.maxCorrectElementsNumber - scoreData.correctElementsNumber - scoreData.emptyElementsNumber;
    }

    presenter.getMaxScore = function () {
        if (!presenter.playerController) {
            return presenter.answers.length;
        }

        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return getMaxScoreFromScoreData();
        }

        checkCorrectnessOfAnswers();
        return getMaxScoreFromScoreData();
    }

    function getMaxScoreFromScoreData() {
        return scoreData.maxCorrectElementsNumber;
    }

    presenter.getScore = function () {
        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return getScoreFromScoreData();
        }

        handleShownAnswers();
        checkCorrectnessOfAnswers();
        return getScoreFromScoreData();
    };

    function getScoreFromScoreData() {
        return scoreData.correctElementsNumber;
    }

    function checkCorrectnessOfAnswers() {
        if (!presenter.answers || !presenter.modulesIDs) {
            return;
        }

        resetScoreData();

        hideMathJaxElements();
        presenter.modulesIDs.forEach(checkCorrectnessOfAnswersInModule);
    }

    function checkCorrectnessOfAnswersInModule(moduleID) {
        const moduleGaps = findModuleGaps(moduleID);
        if (!moduleGaps) {
            return;
        }

        scoreData.maxCorrectElementsNumber += moduleGaps.length;
        moduleGaps.forEach(gap =&gt; {
            if (isGapEmpty(gap)) {
                scoreData.emptyElementsNumber++;
                return;
            }

            for (let answerIndex = 0; answerIndex &lt; presenter.answers.length; answerIndex++) {
                if (isAnswerUsed(answerIndex)) {
                    continue;
                }

                const correctAnswer = presenter.answers[answerIndex];
                if (gap.innerHTML == correctAnswer || gap.value == correctAnswer) {
                    scoreData.correctElementsNumber++;
                    scoreData.usedAnswersIndex.push(answerIndex);
                    scoreData.gapsIDsWithCorrectAnswer.push(gap.id);
                }
            }
        });
    }

    function resetScoreData() {
        scoreData.emptyElementsNumber = 0;
        scoreData.correctElementsNumber = 0;
        scoreData.maxCorrectElementsNumber = 0;
        scoreData.usedAnswersIndex.length = 0;
        scoreData.gapsIDsWithCorrectAnswer.length = 0;
    }

    function hideMathJaxElements() {
        $('body').find(".MathJax_Preview").each(() =&gt; $(this).style.display = "none");
    }

    function isAnswerUsed(answerIndex) {
        return scoreData.usedAnswersIndex.includes(answerIndex);
    }

    function isCorrectAnswerInGap(gapID) {
        return scoreData.gapsIDsWithCorrectAnswer.includes(gapID);
    }

    presenter.createEventData = function (item, value, score) {
        return {
            'source': "" + presenter.addonID,
            'item': "" + item,
            'value': "" + value,
            'score': "" + score
        };
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore();
    };

    presenter.isOK = function (moduleIndex, gapIndex) {
        const trueModuleIndex = moduleIndex - 1;
        const trueGapIndex = gapIndex - 1;

        handleShownAnswers();

        checkCorrectnessOfAnswers();
        if (presenter.modulesIDs[trueModuleIndex]) {
            const moduleGaps = findModuleGaps(presenter.modulesIDs[trueModuleIndex]);
            if (moduleGaps) {
                const elementID = moduleGaps[trueGapIndex].id;
                if (isCorrectAnswerInGap(elementID)) {
                    return true;
                }
            }
        }
        return false;
    };

    presenter.isAttempted = function () {
        handleShownAnswers();

        return countFilledInItems() &gt; 0;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'isAllOK': presenter.isAllOK,
            'isOK': presenter.isOK,
            'isAttempted': presenter.isAttempted
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.sendAllOKEvent = function () {
        const eventData = presenter.createEventData("all", "", "");
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    }

    function countEmptyItems() {
        return countItems(true);
    }

    function countFilledInItems() {
        return countItems(false);
    }

    function countItems(forEmptyGaps) {
        let count = 0;
        presenter.modulesIDs.forEach(moduleID =&gt; {
            const moduleGaps = findModuleGaps(moduleID);
            moduleGaps.forEach(gap =&gt; {
                const gapEmpty = isGapEmpty(gap);
                if ((forEmptyGaps &amp;&amp; gapEmpty) || (!forEmptyGaps &amp;&amp; !gapEmpty)) {
                    count++;
                }
            });
        });
        return count;
    }

    function handleValueChangedEvent (eventData) {
        const moduleType = eventData.moduleType;
        if (moduleType &amp;&amp; SUPPORTED_MODULES_TYPES.includes(moduleType)) {
            checkCorrectnessOfAnswers();

            const gapData = findGapThatSentValueChangedEvent(eventData);
            if (!gapData.gap || gapData.relativeIndex === -1) {
                return;
            }

            const item = gapData.relativeIndex !== -1 ? gapData.relativeIndex : '';
            const score = isCorrectAnswerInGap(gapData.gap.id) ? 1 : 0;
            const value = eventData.value ? eventData.value : '';

            const newEventData = presenter.createEventData(item, value, score);
            presenter.eventBus.sendEvent('ValueChanged', newEventData);

            if (presenter.isAllOK()) {
                presenter.sendAllOKEvent();
            }
        }
    }

    function findGapThatSentValueChangedEvent(eventData) {
        const notFoundResponse = {
            gap: undefined,
            relativeIndex: -1,
        }

        if (eventData.source === undefined || eventData.item === undefined) {
            return notFoundResponse;
        }

        const moduleIndex = presenter.modulesIDs.findIndex(moduleID =&gt; moduleID === eventData.source);
        if (moduleIndex === -1) {
            return notFoundResponse;
        }

        const foundGaps = findModuleGaps(eventData.source);
        const itemIndex = parseInt(eventData.item, 10) - 1;
        if (itemIndex &lt; 0 || itemIndex &gt;= foundGaps.length) {
            return notFoundResponse;
        }

        let relativeIndex = 1;
        for (let i = 0, moduleGaps; i &lt; moduleIndex; i++) {
            moduleGaps = findModuleGaps(presenter.modulesIDs[i]);
            relativeIndex += moduleGaps.length;
        }
        relativeIndex += itemIndex;

        return {
            gap: foundGaps[itemIndex],
            relativeIndex,
        };
    }

    function findModuleGaps(moduleID, toArray = true) {
        const module = getModule(moduleID);
        const $moduleView = $(module.getView());
        const $foundGaps = findGaps($moduleView);
        if (toArray) {
            return $foundGaps.toArray();
        }
        return $foundGaps;
    }

    function getModule(moduleID) {
        return presenter.playerController.getModule(moduleID);
    }

    function findCurrentPageGaps() {
        const $pageView = $('html');
        return findGaps($pageView);
    }

    function findGaps($view) {
        return $view.find('.ic_gap, .gapFilled, .ic_gap-empty, .ic_filled_gap,  .ic_draggableGapFilled, .ic_draggableGapEmpty');
    }

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }
        presenter.hideAnswers();
        presenter.setWorkMode();
    };

    presenter.getState = function () {
        handleShownAnswers();
        checkCorrectnessOfAnswers();
    };

    return presenter;
}
</presenter></addon><addon id="GeometricConstruct">
	<model>
		<property name="strokeColor" nameLabel="GeometricConstruct_property_stroke_color" type="string"/>
		<property name="fillColor" nameLabel="GeometricConstruct_property_fill_color" type="string"/>
		<property displayName="Figures" name="figures" nameLabel="GeometricConstruct_property_figures" type="staticlist">

			<property name="Point" nameLabel="GeometricConstruct_property_point" type="staticrow">
				<property name="Point" nameLabel="GeometricConstruct_property_point" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="Circle" nameLabel="GeometricConstruct_property_circle" type="staticrow">
				<property name="Circle" nameLabel="GeometricConstruct_property_circle" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="CircleWithPoint" nameLabel="GeometricConstruct_property_circle_with_point" type="staticrow">
				<property name="CircleWithPoint" nameLabel="GeometricConstruct_property_circle_with_point" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="Compasses" nameLabel="GeometricConstruct_property_compasses" type="staticrow">
				<property name="Compasses" nameLabel="GeometricConstruct_property_compasses" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="ArcWithCenterPoint" nameLabel="GeometricConstruct_property_arc_with_center_point" type="staticrow">
				<property name="ArcWithCenterPoint" nameLabel="GeometricConstruct_property_arc_with_center_point" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="Angle" nameLabel="GeometricConstruct_property_angle" type="staticrow">
				<property name="Angle" nameLabel="GeometricConstruct_property_angle" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="LineSegment" nameLabel="GeometricConstruct_property_line_segment" type="staticrow">
				<property name="LineSegment" nameLabel="GeometricConstruct_property_line_segment" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="HalfOpenLineSegment" nameLabel="GeometricConstruct_property_half_open_line_segment" type="staticrow">
				<property name="HalfOpenLineSegment" nameLabel="GeometricConstruct_property_half_open_line_segment" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

			<property name="OpenLineSegment" nameLabel="GeometricConstruct_property_open_line_segment" type="staticrow">
				<property name="OpenLineSegment" nameLabel="GeometricConstruct_property_open_line_segment" type="string"/>
				<property name="Disabled" nameLabel="GeometricConstruct_property_disabled" type="boolean"/>
			</property>

		</property>
		<property displayName="Labels" name="labels" nameLabel="GeometricConstruct_property_labels" type="staticlist">

			<property name="Cursor" nameLabel="GeometricConstruct_property_cursor" type="staticrow">
				<property name="Cursor" nameLabel="GeometricConstruct_property_cursor" type="string"/>
			</property>

			<property name="CirclePopupTitle" nameLabel="GeometricConstruct_property_circle_popup_title" type="staticrow">
				<property name="CirclePopupTitle" nameLabel="GeometricConstruct_property_circle_popup_title" type="string"/>
			</property>

			<property name="Accept" nameLabel="GeometricConstruct_property_accept" type="staticrow">
				<property name="Accept" nameLabel="GeometricConstruct_property_accept" type="string"/>
			</property>

			<property name="Cancel" nameLabel="GeometricConstruct_property_cancel" type="staticrow">
				<property name="Cancel" nameLabel="GeometricConstruct_property_cancel" type="string"/>
			</property>

			<property name="Radius" nameLabel="GeometricConstruct_property_radius" type="staticrow">
				<property name="Radius" nameLabel="GeometricConstruct_property_radius" type="string"/>
			</property>

		</property>
		<property name="labelsVisibility" nameLabel="GeometricConstruct_property_labels_visibility" type="boolean"/>
		<property name="angleMeasuresVisibility" nameLabel="GeometricConstruct_property_angle_measures_visibility" type="boolean"/>
		<property name="DisableUndoRedoButton" nameLabel="GeometricConstruct_property_disable_undo_redo" type="boolean"/>
		<property name="DisableResetButton" nameLabel="GeometricConstruct_property_disable_reset_button" type="boolean"/>
		<property name="DisableLabelToggle" nameLabel="GeometricConstruct_property_disable_label_toggle" type="boolean"/>
		<property name="xAxisPosition" nameLabel="GeometricConstruct_property_x_axis_position" type="string"/>
		<property name="yAxisPosition" nameLabel="GeometricConstruct_property_y_axis_position" type="string"/>
		<property name="axisIncrement" nameLabel="GeometricConstruct_property_axis_increment" type="string"/>
		<property name="unitLength" nameLabel="GeometricConstruct_property_unit_length" type="string"/>
		<property name="axisColor" nameLabel="GeometricConstruct_property_axis_color" type="string"/>
		<property name="gridColor" nameLabel="GeometricConstruct_property_grid_color" type="string"/>
		<property name="hideXAxis" nameLabel="GeometricConstruct_property_hide_x_axis" type="boolean"/>
		<property name="hideYAxis" nameLabel="GeometricConstruct_property_hide_y_axis" type="boolean"/>
		<property name="angleDecimalPoint" nameLabel="GeometricConstruct_property_angle_decimal_point" type="string"/>
		<property name="lengthDecimalPoint" nameLabel="GeometricConstruct_property_length_decimal_point" type="string"/>
		<property name="lengthMeasuresVisibility" nameLabel="GeometricConstruct_property_length_visibility" type="boolean"/>

	</model>
<css>.geometric_construct_wrapper {
    height: 100%;
    display: flex;
}

.geometric_construct_wrapper .toolbar_wrapper {
    height: 100%;
    width: 220px;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbar_options {
    height: calc(100% - 36px);
    width: 220px;
    overflow-y: auto;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbar_options .toolbar_section {
    display: flex;
    flex-wrap: wrap;
    position: relative;
}

.geometric_construct_wrapper .toolbar_wrapper .static_tools {
    display: block;
    width: 100%;
    height: 35px;
    position: absolute;
    bottom: 0px;
}

.geometric_construct_wrapper .toolbar_wrapper .static_tools .undo_button {
    display: inline-block;
    width: 30px;
    height: 30px;
    background: url('resources/undo_24dp.svg') no-repeat center;
    background-size: 35px;
    border: none;
}

.geometric_construct_wrapper .toolbar_wrapper .static_tools .redo_button {
    display: inline-block;
    width: 30px;
    height: 30px;
    background: url('resources/redo_24dp.svg') no-repeat center;
    background-size: 35px;
    border: none;
}

.geometric_construct_wrapper .toolbar_wrapper .static_tools .reset_button {
    display: inline-block;
    width: 30px;
    height: 30px;
    background: url('resources/restart_alt_24dp.svg') no-repeat center;
    background-size: 35px;
    border: none;
}

.geometric_construct_wrapper .toolbar_wrapper .static_tools .labels_button {
    display: inline-block;
    width: 30px;
    height: 30px;
    background: url('resources/gc_labels_visibility_24dp.svg') no-repeat center;
    background-size: 35px;
    border: none;
}

.geometric_construct_wrapper .workspace_wrapper {
    display: inline-block;
    height: 100%;
    flex-grow: 1;
    border: 1px solid;
    position: relative;
    overflow: hidden;
}

.geometric_construct_wrapper .workspace_wrapper canvas {
    height: 100%;
    width: 100%;
}

.geometric_construct_wrapper .workspace_wrapper .canvas_overlay {
    height: 100%;
    width: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
}

.geometric_construct_wrapper .workspace_wrapper .remove_figure {
    display: block;
    height: 35px;
    width: 30px;
    position: absolute;
    right: 10px;
    bottom: 10px;
    background: url('resources/delete_24dp.svg') no-repeat center;
    background-size: 40px;
    border: none;
}

.geometric_construct_wrapper .workspace_wrapper .edit_figure {
    display: block;
    height: 35px;
    width: 30px;
    position: absolute;
    right: 10px;
    bottom: 55px;
    background: url('resources/edit_24dp.svg') no-repeat center;
    background-size: 40px;
}

.geometric_construct_wrapper .workspace_wrapper .edit_figure {
    display: block;
    height: 35px;
    width: 30px;
    position: absolute;
    right: 10px;
    bottom: 55px;
    background: url('resources/edit_24dp.svg') no-repeat center;
    background-size: 40px;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton {
    display: inline-block;
    max-width: 80px;
    min-height: 80px;
    margin: 10px 5px 5px 5px;
    padding: 5px;
    border: none;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon {
    display: block;
    width: 40px;
    height: 40px;
    margin-left: auto;
    margin-right: auto;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.cursor-image {
    background: url('resources/pan_tool_alt_24dp.svg') no-repeat center;
    background-size: 30px;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.point-image {
    background: url('resources/gc_point_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.line-segment-image {
    background: url('resources/gc_line_segment_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.half-open-line-segment-image {
    background: url('resources/gc_half_open_line_segment_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.open-line-segment-image {
    background: url('resources/gc_open_line_segment_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.circle-image {
    background: url('resources/gc_circle_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.circle-with-point-image {
    background: url('resources/gc_circle_with_point_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.compasses-image {
    background: url('resources/gc_compasses_icon.png') no-repeat center;
    background-size: 40px;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.arc-with-center-point-image {
    background: url('resources/gc_arc_with_center_point_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .icon.angle-image {
    background: url('resources/gc_angle_icon.png') no-repeat center;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton canvas {
    display: block;
    margin-left: auto;
    margin-right: auto;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton .toolbar-button-label {
    text-align: center;
    font-size: 0.7em;
}

.geometric_construct_wrapper .toolbar_wrapper .toolbarButton.selected {
    border: 1px solid;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup {
    height: 180px;
    width: 300px;
    position: absolute;
    display: flex;
    flex-direction: column;
    background-color: white;
    border: solid 1px black;
    font-size: 0.8em;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .title {
    padding: 5px;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .values {
    flex-grow: 1;
    padding: 10px 0px;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .values .property_wrapper {
    padding: 10px;
    width: calc(100%-20px);
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .values .property_wrapper .property_title {
    font-size: 0.6em;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .values .property_wrapper input {
    width: 100%;
    border: 0px;
    border-bottom: 1px solid;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .buttons {
    display: flex;
    padding: 5px;
}

.geometric_construct_wrapper .workspace_wrapper .config_popup .buttons button {
    display: inline-block;
    margin-left: auto;
    margin-right: auto;
    background-color: white;
    border: solid 1px black;
    padding: 3px;
}

.geometric_construct_wrapper .workspace_wrapper .axis_label {
    font-size: 0.7em;
    color: #444444;
}
</css><view>&lt;div class="geometric_construct_wrapper"&gt;
    &lt;div class="toolbar_wrapper"&gt;
        &lt;div class="toolbar_options"&gt;&lt;/div&gt;
        &lt;div class="static_tools"&gt;
            &lt;button class="undo_button"&gt;&lt;/button&gt;
            &lt;button class="redo_button"&gt;&lt;/button&gt;
            &lt;button class="reset_button"&gt;&lt;/button&gt;
            &lt;button class="labels_button"&gt;&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="workspace_wrapper"&gt;
        &lt;div class="canvas_overlay"&gt;&lt;/div&gt;
        &lt;div class="remove_figure"&gt;&lt;/div&gt;
        &lt;div class="edit_figure"&gt;&lt;/div&gt;
        &lt;div class="config_popup"&gt;
            &lt;div class="title"&gt;&lt;/div&gt;
            &lt;div class="values"&gt;&lt;/div&gt;
            &lt;div class="buttons"&gt;
                &lt;button class="cancel"&gt;&lt;/button&gt;
                &lt;button class="accept"&gt;&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="geometric_construct_wrapper"&gt;
    &lt;div class="toolbar_wrapper"&gt;
        &lt;div class="toolbar_options"&gt;&lt;/div&gt;
        &lt;div class="static_tools"&gt;
            &lt;button class="undo_button"&gt;&lt;/button&gt;
            &lt;button class="redo_button"&gt;&lt;/button&gt;
            &lt;button class="reset_button"&gt;&lt;/button&gt;
            &lt;button class="labels_button"&gt;&lt;/button&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="workspace_wrapper"&gt;
        &lt;div class="canvas_overlay"&gt;&lt;/div&gt;
        &lt;div class="remove_figure"&gt;&lt;/div&gt;
        &lt;div class="edit_figure"&gt;&lt;/div&gt;
        &lt;div class="config_popup"&gt;
            &lt;div class="title"&gt;&lt;/div&gt;
            &lt;div class="values"&gt;&lt;/div&gt;
            &lt;div class="buttons"&gt;
                &lt;button class="cancel"&gt;&lt;/button&gt;
                &lt;button class="accept"&gt;&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonGeometricConstruct_create() {
    var presenter = function () {};

    presenter.labelsList = [];
    presenter.greekLabelsList = [];
    presenter.lowerLabelsList = [];
    presenter.figuresList = [];
    presenter.pointsList = [];
    presenter.maxPointIndex = 0;
    presenter.toolbarButtonsDict = {};
    presenter.labelsVisibility = true;
    presenter.angleMeasuresVisibility = true;
    presenter.lengthMeasuresVisibility = true;
    presenter.axisLabels = {
        x: {},
        y: {}
    }

    var MAX_PREV_STATES = 20;
    presenter.prevStateIndex = -1;
    presenter.previousStates = [];

    presenter.newFigure = null;
    presenter.editFigure = null;
    presenter.isEditMode = false;
    presenter.isDragging = false;
    presenter.hasMoved = false;
    var originalConfigPopupDisplay = '';

    presenter.playerController = null;

    presenter.CSS_CLASSES = {
        SELECTED: 'selected',
        TOOLBAR_WRAPPER: 'toolbar_wrapper',
        WORKSPACE_WRAPPER: 'workspace_wrapper',
        CANVAS_OVERLAY: 'canvas_overlay',
        REMOVE_FIGURE_BUTTON: 'remove_figure',
        EDIT_FIGURE_BUTTON: 'edit_figure',
        CURSOR: 'Cursor',
        CURSOR_IMAGE: 'cursor-image',
        UNDO_BUTTON: 'undo_button',
        REDO_BUTTON: 'redo_button',
        RESET_BUTTON: 'reset_button',
        LABELS_BUTTON: 'labels_button',
        LABELS_BUTTON_SHOW: 'show',
        LABELS_BUTTON_HIDE: 'hide',
        LABEL: 'geometricConstructLabel',
        TOOLBAR_BUTTON: 'toolbarButton',
        TOOLBAR_BUTTON_LABEL: 'toolbar-button-label',
        TOOLBAR_BUTTON_ICON: 'icon',
        TOOLBAR_OPTIONS: 'toolbar_options',
        TOOLBAR_SECTION: 'toolbar_section',
        TOOLBAR_BUTTON: 'toolbarButton',
        CONFIG_POPUP: 'config_popup',
        CONFIG_POPUP_TITLE: 'title',
        CONFIG_POPUP_VALUES: 'values',
        CONFIG_POPUP_ACCEPT: 'accept',
        CONFIG_POPUP_CANCEL: 'cancel',
        CONFIG_PROP_WRAPPER: 'property_wrapper',
        CONFIG_PROP_TITLE: 'property_title',
        CONFIG_PROP_INPUT: 'property_input',
        AXIS_LABEL: 'axis_label'
    };

    presenter.labels = {
        point: "Point",
        cursor: "Cursor",
        circle: "Circle with a specified radius",
        circleWithPoint: "Circle passing through a point",
        compasses: "Compasses",
        arcWithCenterPoint: "Arc with a defined center",
        angle: "Angle",
        lineSegment: "Line Segment",
        halfOpenLineSegment: "Half-open Line Segment",
        openLineSegment: "Open Line Segment",
        circlePopupTitle: "Circle with a specified radius",
        accept: "ACCEPT",
        cancel: "CANCEL",
        radius: "Radius"
    }

    presenter.ERROR_CODES = {
        'IV_01': "Axis increment must be a positive number or left empty",
        'IV_02': "Unit length must be a positive number or left empty",
        'IV_03': "X and Y axis position must be a positive number or left empty",
        'IV_04': "Angle's decimal point must be an non-negative integer or left empty",
        'IV_05': "Length's decimal point must be an non-negative integer or left empty"
    }

    presenter.enabledFigures = {
        point: true,
        circle: true,
        circleWithPoint: true,
        compasses: true,
        arcWithCenterPoint: true,
        angle: true,
        lineSegment: true,
        halfOpenLineSegment: true,
        openLineSegment: true
    }

    var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    var LOWER_ALPHABET = ALPHABET.toLowerCase();
    var GREEK_ALPHABET = "αβγδεζηθικλμνξοπρστυφχψω";

    presenter.createPreview = function (view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function (view, model) {
        presenterLogic(view, model, false);
    };

    function presenterLogic(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            $(view).html(presenter.ERROR_CODES[presenter.configuration.errorCode]);
            return;
        }
        presenter.setElements(view);
        presenter.createView(isPreview);
        if (!presenter.configuration.defaultVisibility) presenter.hide();
        presenter.setLabelsVisibility(presenter.configuration.labelsDefaultVisibility);
        presenter.setAngleMeasuresVisibility(presenter.configuration.angleMeasuresDefaultVisibility);
        presenter.setLengthMeasuresVisibility(presenter.configuration.lengthMeasuresDefaultVisibility);
        presenter.pushState();
    }

    presenter.show = function() {
        presenter.$view.css('visibility', '');
        presenter.$view.css('display', '');
    }

    presenter.hide = function() {
        presenter.$view.css('visibility', 'hidden');
        presenter.$view.css('display', 'none');
    }

    presenter.isVisible = function() {
        return presenter.$view.css('display') != 'none' &amp;&amp;  presenter.$view.css('visibility') != 'hidden';
    }

    presenter.showLabels = function() {
        presenter.setLabelsVisibility(true);
    }

    presenter.hideLabels = function() {
        presenter.setLabelsVisibility(false);
    }

    presenter.setLabelsVisibility = function(visible) {
        presenter.labelsVisibility = visible;
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            var figure = presenter.figuresList[i];
            if (presenter.labelsVisibility) {
                figure.showLabel();
            } else {
                figure.hideLabel();
            }
        }
        if (!presenter.configuration.disableLabelToggle) {
            if (presenter.labelsVisibility) {
                presenter.$labelsButton.removeClass(presenter.CSS_CLASSES.LABELS_BUTTON_SHOW);
                presenter.$labelsButton.addClass(presenter.CSS_CLASSES.LABELS_BUTTON_HIDE);
            } else {
                presenter.$labelsButton.removeClass(presenter.CSS_CLASSES.LABELS_BUTTON_HIDE);
                presenter.$labelsButton.addClass(presenter.CSS_CLASSES.LABELS_BUTTON_SHOW);
            }
        }
    }

    presenter.showAngleMeasures = function() {
        presenter.setAngleMeasuresVisibility(true);
    }

    presenter.hideAngleMeasures = function() {
        presenter.setAngleMeasuresVisibility(false);
    }

    presenter.setAngleMeasuresVisibility = function(visible) {
        presenter.angleMeasuresVisibility = visible;
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            var figure = presenter.figuresList[i];
            if (figure.showAngleMeasure !== undefined) {
                if (presenter.angleMeasuresVisibility) {
                    figure.showAngleMeasure();
                } else {
                    figure.hideAngleMeasure();
                }
            }
        }
    }

    presenter.showLengthMeasures = function() {
        presenter.setLengthMeasuresVisibility(true);
    }

    presenter.hideLengthMeasures = function() {
        presenter.setLengthMeasuresVisibility(false);
    }

    presenter.setLengthMeasuresVisibility = function(visible) {
        presenter.lengthMeasuresVisibility = visible;
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            var figure = presenter.figuresList[i];
            if (figure.showLengthMeasure !== undefined) {
                if (presenter.lengthMeasuresVisibility) {
                    figure.showLengthMeasure();
                } else {
                    figure.hideLengthMeasure();
                }
            }
        }
    }

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeAxisConfig(model);
        upgradedModel = presenter.upgradeAngle(upgradedModel);
        upgradedModel = presenter.upgradeHideAxes(upgradedModel);
        upgradedModel = presenter.upgradeAngleMeasuresVisibility(upgradedModel);
        upgradedModel = presenter.upgradeLengthMeasure(upgradedModel);
        return presenter.upgradeGridColor(upgradedModel);
    }

    presenter.upgradeAngle = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["figures"]["Angle"] === undefined) {
            upgradedModel["figures"]["Angle"] =  {
                "Angle": "",
                "Disabled": ""
            };
        }
        if (upgradedModel["angleDecimalPoint"] == undefined) {
            upgradedModel["angleDecimalPoint"] = "";
        }

        return upgradedModel;
    }

    presenter.upgradeAxisConfig = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object
        if (upgradedModel["axisIncrement"] === undefined) {
            upgradedModel["axisIncrement"] =  '';
        }
        if (upgradedModel["xAxisPosition"] === undefined) {
            upgradedModel["xAxisPosition"] =  '';
        }
        if (upgradedModel["yAxisPosition"] === undefined) {
            upgradedModel["yAxisPosition"] =  '';
        }
        if (upgradedModel["axisColor"] === undefined) {
            upgradedModel["axisColor"] =  '';
        }
        if (upgradedModel["unitLength"] === undefined) {
            upgradedModel["unitLength"] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeHideAxes = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["hideXAxis"] == undefined) {
            upgradedModel["hideXAxis"] = "";
        }
        if (upgradedModel["hideYAxis"] == undefined) {
            upgradedModel["hideYAxis"] = "";
        }

        return upgradedModel;
    }

    presenter.upgradeAngleMeasuresVisibility = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["angleMeasuresVisibility"] == undefined) {
            upgradedModel["angleMeasuresVisibility"] = "";
        }

        return upgradedModel;
    }

    presenter.upgradeLengthMeasure = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["lengthDecimalPoint"] == undefined) {
            upgradedModel["lengthDecimalPoint"] = "";
        }

        if (upgradedModel["lengthMeasuresVisibility"] == undefined) {
            upgradedModel["lengthMeasuresVisibility"] = "";
        }

        return upgradedModel;
    }
          
    presenter.upgradeGridColor = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["gridColor"] == undefined) {
            upgradedModel["gridColor"] = "";
        }

        return upgradedModel;
    }

    presenter.getActualUnitLength = function() {
        return presenter.configuration.unitLength;
    }

    presenter.convertToUnit = function(value) {
        return value / presenter.getActualUnitLength();
    }

    presenter.validatePositiveInteger = function (rawValue, defaultValue, errorCode) {
        var resultValue = defaultValue;
        if (rawValue.trim().length != 0) {
            var result = ModelValidationUtils.validateInteger(rawValue);
            if (!result.isValid || result.value &lt;= 0) {
                return {isValid: false, errorCode: errorCode};
            }
            return result;
        }
        return {isValid: true, value: resultValue};
    }


    presenter.validateModel = function (model) {
        var strokeColor = (model["strokeColor"] &amp;&amp; model["strokeColor"].trim().length &gt; 0) ? model["strokeColor"] : "black";
        var fillColor = (model["fillColor"] &amp;&amp; model["fillColor"].trim().length &gt; 0) ? model["fillColor"] : "blue";
        var axisColor = (model["axisColor"] &amp;&amp; model["axisColor"].trim().length &gt; 0) ? model["axisColor"] : "#444444";

        var axisIncrementResult = presenter.validatePositiveInteger(model['axisIncrement'], 1, 'IV_01');
        if (!axisIncrementResult.isValid) return axisIncrementResult;
        var axisIncrement = axisIncrementResult.value;

        var unitLengthResult = presenter.validatePositiveInteger(model['unitLength'], 25, 'IV_02');
        if (!unitLengthResult.isValid) return unitLengthResult;
        var unitLength = unitLengthResult.value;

        var xAxisPositionResult = presenter.validatePositiveInteger(model['xAxisPosition'], 0, 'IV_03');
        if (!xAxisPositionResult.isValid) return xAxisPositionResult;
        var xAxisPosition = xAxisPositionResult.value;

        var yAxisPositionResult = presenter.validatePositiveInteger(model['yAxisPosition'], 0, 'IV_03');
        if (!yAxisPositionResult.isValid) return yAxisPositionResult;
        var yAxisPosition = yAxisPositionResult.value;

        setLabels(model["labels"], model["figures"]);
        setEnabledFigures(model["figures"]);

        var angleDecimalPoint = 0;
        if (model["angleDecimalPoint"].trim().length &gt; 0) {
            var angleDecimalPointResult = ModelValidationUtils.validateInteger(model["angleDecimalPoint"]);
            if (!angleDecimalPointResult.isValid || angleDecimalPointResult.value &lt; 0) {
                return {isValid: false, errorCode: "IV_04"};
            }
            angleDecimalPoint = angleDecimalPointResult.value;
        }
        
        var lengthDecimalPoint = 0;
        if (model["lengthDecimalPoint"].trim().length &gt; 0) {
            var lengthDecimalPointResult = ModelValidationUtils.validateInteger(model["lengthDecimalPoint"]);
            if (!lengthDecimalPointResult.isValid || lengthDecimalPointResult.value &lt; 0) {
                return {isValid: false, errorCode: "IV_05"};
            }
            lengthDecimalPoint = lengthDecimalPointResult.value;
        }

        return {
            isValid: true,
            fillColor: fillColor,
            strokeColor: strokeColor,
            width: parseInt(model["Width"]),
            height: parseInt(model["Height"]),
            defaultVisibility: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            labelsDefaultVisibility: ModelValidationUtils.validateBoolean(model["labelsVisibility"]),
            angleMeasuresDefaultVisibility: ModelValidationUtils.validateBoolean(model["angleMeasuresVisibility"]),
            disableUndoRedoButton: ModelValidationUtils.validateBoolean(model["DisableUndoRedoButton"]),
            disableResetButton: ModelValidationUtils.validateBoolean(model["DisableResetButton"]),
            disableLabelToggle: ModelValidationUtils.validateBoolean(model["DisableLabelToggle"]),
            axisColor: axisColor,
            axisIncrement: axisIncrement,
            unitLength: unitLength,
            xAxisPosition: xAxisPosition,
            yAxisPosition: yAxisPosition,
            angleDecimalPoint: angleDecimalPoint,
            hideXAxis: ModelValidationUtils.validateBoolean(model["hideXAxis"]),
            hideYAxis: ModelValidationUtils.validateBoolean(model["hideYAxis"]),
            lengthDecimalPoint: lengthDecimalPoint,
            lengthMeasuresDefaultVisibility: ModelValidationUtils.validateBoolean(model["lengthMeasuresVisibility"]),
            gridColor: model["gridColor"]
        };
    }
    
    function getLabelValue (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setLabels(labels, figures) {
        if (!labels || !figures) {
            return;
        }

        presenter.labels = {
            point: getLabelValue(figures['Point']['Point'], presenter.labels.point),
            cursor: getLabelValue(labels['Cursor']['Cursor'], presenter.labels.cursor),
            circle: getLabelValue(figures['Circle']['Circle'], presenter.labels.circle),
            circleWithPoint: getLabelValue(figures['CircleWithPoint']['CircleWithPoint'], presenter.labels.circleWithPoint),
            compasses: getLabelValue(figures['Compasses']['Compasses'], presenter.labels.compasses),
            arcWithCenterPoint: getLabelValue(figures['ArcWithCenterPoint']['ArcWithCenterPoint'], presenter.labels.arcWithCenterPoint),
            angle: getLabelValue(figures['Angle']['Angle'], presenter.labels.angle),
            lineSegment: getLabelValue(figures['LineSegment']['LineSegment'], presenter.labels.lineSegment),
            halfOpenLineSegment: getLabelValue(figures['HalfOpenLineSegment']['HalfOpenLineSegment'], presenter.labels.halfOpenLineSegment),
            openLineSegment: getLabelValue(figures['OpenLineSegment']['OpenLineSegment'], presenter.labels.openLineSegment),
            circlePopupTitle: getLabelValue(labels['CirclePopupTitle']['CirclePopupTitle'], presenter.labels.circlePopupTitle),
            accept: getLabelValue(labels['Accept']['Accept'], presenter.labels.accept),
            cancel: getLabelValue(labels['Cancel']['Cancel'], presenter.labels.cancel),
            radius: getLabelValue(labels['Radius']['Radius'], presenter.labels.radius)
        };
    }

    function setEnabledFigures(figures) {
        if (!figures) return;

        presenter.enabledFigures = {
            point: !ModelValidationUtils.validateBoolean(figures['Point']["Disabled"]),
            circle: !ModelValidationUtils.validateBoolean(figures['Circle']["Disabled"]),
            circleWithPoint: !ModelValidationUtils.validateBoolean(figures['CircleWithPoint']["Disabled"]),
            compasses: !ModelValidationUtils.validateBoolean(figures['Compasses']["Disabled"]),
            arcWithCenterPoint: !ModelValidationUtils.validateBoolean(figures['ArcWithCenterPoint']["Disabled"]),
            angle: !ModelValidationUtils.validateBoolean(figures['Angle']["Disabled"]),
            lineSegment: !ModelValidationUtils.validateBoolean(figures['LineSegment']["Disabled"]),
            halfOpenLineSegment: !ModelValidationUtils.validateBoolean(figures['HalfOpenLineSegment']["Disabled"]),
            openLineSegment: !ModelValidationUtils.validateBoolean(figures['OpenLineSegment']["Disabled"])
        }
    }

    presenter.setElements = function(view) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.$toolbarWrapper = presenter.$view.find('.'+presenter.CSS_CLASSES.TOOLBAR_WRAPPER);
        presenter.$undoButton = presenter.$toolbarWrapper.find('.'+presenter.CSS_CLASSES.UNDO_BUTTON);
        presenter.$redoButton = presenter.$toolbarWrapper.find('.'+presenter.CSS_CLASSES.REDO_BUTTON);
        presenter.$resetButton = presenter.$toolbarWrapper.find('.'+presenter.CSS_CLASSES.RESET_BUTTON);
        presenter.$labelsButton = presenter.$toolbarWrapper.find('.'+presenter.CSS_CLASSES.LABELS_BUTTON);
        presenter.$toolbarOptions = presenter.$view.find('.'+presenter.CSS_CLASSES.TOOLBAR_OPTIONS);
        presenter.$workspaceWrapper = presenter.$view.find('.'+presenter.CSS_CLASSES.WORKSPACE_WRAPPER);
        presenter.workspaceWrapper = presenter.$workspaceWrapper[0];
    }

    presenter.createView = function(isPreview) {
        presenter.createWorkspace(isPreview);
        presenter.createToolbar();
        presenter.redrawCanvas();
    };

    presenter.createWorkspace = function(isPreview) {
        presenter.canvas = document.createElement("canvas");
        presenter.$canvas = $(presenter.canvas);
        presenter.canvasWidth = presenter.$workspaceWrapper.width();
        presenter.canvasHeight = presenter.configuration.height;
        presenter.canvas.setAttribute('width', presenter.canvasWidth);
        presenter.canvas.setAttribute('height', presenter.canvasHeight);
        presenter.context = presenter.canvas.getContext("2d");
        presenter.$workspaceWrapper.prepend(presenter.canvas);
        presenter.canvasRect = presenter.canvas.getBoundingClientRect();
        presenter.$canvasOverlay = presenter.$workspaceWrapper.find("."+presenter.CSS_CLASSES.CANVAS_OVERLAY);
        presenter.$canvasOverlay.css('width', presenter.canvasWidth);
        presenter.$canvasOverlay.css('height', presenter.canvasHeight);
        if (!isPreview) {
            presenter.$canvasOverlay.on('mousedown', canvasOnMouseDownHandler);
            presenter.$canvasOverlay.on('touchstart', canvasOnMouseDownHandler);
            presenter.$canvasOverlay.on('mousemove', canvasOnMouseMoveHandler);
            presenter.$canvasOverlay.on('touchmove', canvasOnMouseMoveHandler);
            presenter.$canvasOverlay.on('mouseup', canvasOnMouseUpHandler);
            presenter.$canvasOverlay.on('touchend', canvasOnMouseUpHandler);
        }

        presenter.$removeFigure = presenter.$workspaceWrapper.find("."+presenter.CSS_CLASSES.REMOVE_FIGURE_BUTTON);
        presenter.$removeFigure.css('display', 'none');
        presenter.$editFigure = presenter.$workspaceWrapper.find("."+presenter.CSS_CLASSES.EDIT_FIGURE_BUTTON);
        presenter.$editFigure.css('display', 'none');
        if (!isPreview) {
            presenter.$removeFigure.on('click', removeFigureOnClickHandler);
            presenter.$editFigure.on('click', editFigureOnClickHandler);
        }
        presenter.$configPopup = presenter.$workspaceWrapper.find('.'+presenter.CSS_CLASSES.CONFIG_POPUP);
        var popupRect = presenter.$configPopup[0].getBoundingClientRect();
        var popupTop = Math.round((presenter.canvasHeight - popupRect.height)/2);
        if (popupTop &lt; 0) popupTop = 0;
        var popupLeft = Math.round((presenter.canvasWidth - popupRect.width)/2);
        if (popupLeft &lt; 0) popupLeft = 0;
        presenter.$configPopup.css('top', popupTop + 'px');
        presenter.$configPopup.css('left', popupLeft + 'px');
        var originalConfigPopupDisplay = presenter.$configPopup.css('display');
        presenter.hideConfigPopup();
        presenter.$configPopupTitle = presenter.$configPopup.find('.'+presenter.CSS_CLASSES.CONFIG_POPUP_TITLE);
        presenter.$configPopupValues = presenter.$configPopup.find('.'+presenter.CSS_CLASSES.CONFIG_POPUP_VALUES);
        presenter.$configPopupAccept = presenter.$configPopup.find('.'+presenter.CSS_CLASSES.CONFIG_POPUP_ACCEPT);
        presenter.$configPopupCancel = presenter.$configPopup.find('.'+presenter.CSS_CLASSES.CONFIG_POPUP_CANCEL);
        presenter.$configPopupAccept.html(presenter.labels.accept);
        presenter.$configPopupCancel.html(presenter.labels.cancel);
    };

    presenter.clearCanvas = function () {
        presenter.context.clearRect(0, 0, presenter.canvas.width, presenter.canvas.height);
    };

    presenter.clearLabels = function () {
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            presenter.figuresList[i].removeLabel();
        }
    }

    presenter.redrawCanvas = function () {
        presenter.clearCanvas();
        presenter.drawBackground();
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            var f = presenter.figuresList[i];
            f.draw();
            f.addLabel();
        }
    };

    function canvasOnMouseDownHandler (e) {
        e.preventDefault();
        e.stopPropagation();
        var location = getCanvasEventLocation(e);
        if (presenter.newFigure != null) {
            presenter.newFigure.insertClickHandler(e);
            presenter.redrawCanvas();
        } else if (presenter.isEditMode) {
            var selectedFigure = null;
            for (var i = 0; i &lt; presenter.figuresList.length; i++) {
                var figure = presenter.figuresList[i];
                if (figure.isClicked(e)) {
                    selectedFigure = figure;
                    break;
                }
            }
            if (selectedFigure != null) {
                if (presenter.editFigure != selectedFigure) {
                    presenter.clearFigureSelection();
                    presenter.editFigure = selectedFigure;
                    presenter.$removeFigure.css('display', '');
                    if (selectedFigure.isEditButtonAvailable) {
                        presenter.$editFigure.css('display', '');
                    }
                }
                presenter.editFigure.setSelected(true, e);
                presenter.isDragging = true;
                presenter.hasMoved = false;
            } else if (presenter.editFigure != null) {
                presenter.editFigure.setSelected(false);
                presenter.editFigure = null;
                presenter.$removeFigure.css('display', 'none');
                presenter.$editFigure.css('display', 'none');
            }
            presenter.redrawCanvas();
        }
    };

    function canvasOnMouseMoveHandler (e) {
        e.preventDefault();
        e.stopPropagation();
        if (presenter.isEditMode &amp;&amp; presenter.isDragging &amp;&amp; presenter.editFigure != null) {
            presenter.hasMoved = true;
            presenter.editFigure.moveHandler(e);
            presenter.redrawCanvas();
        } else if (presenter.newFigure != null) {
            var redraw = presenter.newFigure.insertMoveHandler(e);
            if (redraw) presenter.redrawCanvas();
        }
    }

    function canvasOnMouseUpHandler (e) {
        e.preventDefault();
        e.stopPropagation();
        if (presenter.isEditMode &amp;&amp; presenter.isDragging) {
            presenter.isDragging = false;
            if (presenter.hasMoved) {
                presenter.hasMoved = false;
                presenter.pushState();
            }
        }
    }

    function removeFigureOnClickHandler (e) {
        if (presenter.editFigure != null) {
            presenter.editFigure.remove();
            presenter.editFigure = null;
            presenter.redrawCanvas();
            presenter.updateLabels();
            presenter.$removeFigure.css('display', 'none');
            presenter.pushState();
            presenter.$editFigure.css('display', 'none');
        }
    }

    function editFigureOnClickHandler (e) {
        if (presenter.editFigure != null &amp;&amp; presenter.editFigure.isEditButtonAvailable) {
            presenter.editFigure.openEditPopup();
        }
    }

    function labelsButtonHandler (e) {
        var newLabelsVisibilityValue = !presenter.labelsVisibility
        presenter.setLabelsVisibility(newLabelsVisibilityValue);
    }

    presenter.updateLabels = function() {
        for (var i = 0; i &lt; presenter.figuresList.length; i++) {
            presenter.figuresList[i].updateLabel();
        }
    }

    function getCanvasEventLocation (e) {
        var scale = {scaleX: 1.0, scaleY: 1.0};
        if (presenter.playerController) {
            scale = presenter.playerController.getScaleInformation();
        }
        var pageX = 0;
        var pageY = 0;
        if (e.type == "mousedown" || e.type == "mousemove" || e.type == "mouseup") {
            pageX = e.pageX;
            pageY = e.pageY;
        } else if (e.originalEvent &amp;&amp; e.originalEvent.touches &amp;&amp; e.originalEvent.touches.length &gt; 0) {
            var touch = e.originalEvent.touches[0];
            pageX = touch.pageX;
            pageY = touch.pageY;
        }
        var result = {
            x: (pageX - presenter.$canvas.offset().left) / scale.scaleX,
            y: (pageY - presenter.$canvas.offset().top) / scale.scaleY
        };
        if (result.x &lt; 0) result.x = 0;
        if (result.x &gt; presenter.canvasWidth) result.x = presenter.canvasWidth;
        if (result.y &lt; 0) result.y = 0;
        if (result.y &gt; presenter.canvasHeight) result.y = presenter.canvasHeight;
        return result;
    }

    presenter.getClickedPoint = function(event) {
        for (var i = 0; i &lt; presenter.pointsList.length; i++) {
            var point = presenter.pointsList[i];
            if (point.isClicked(event)) return point;
        }
        return null;
    }

    presenter.getClickedOrCreatePoint = function(event) {
        var point;
        var clickedPoint = presenter.getClickedPoint(event);
        if (clickedPoint != null) {
            point = clickedPoint;
        } else {
            var location = getCanvasEventLocation(event);
            point = new Point();
            point.setLocation(location.x, location.y);
        }
        return point;
    }

    presenter.getAngle = function(x1, y1, x2, y2) {
        return Math.atan2(y2 - y1, x2 - x1);
    }

    presenter.getPointByID = function(id) {
        for (var i = 0; i &lt; presenter.pointsList.length; i++) {
            var point = presenter.pointsList[i];
            if (point.id == id) return point;
        }
        return null;
    }

    presenter.getOrCreateChildPointByID = function(id, parent) {
        var point;
        var existingPoint = presenter.getPointByID(id);
        if (existingPoint != null) {
            point = existingPoint;
        } else {
            point = new Point();
            point.setIsRoot(false);
        }
        point.addParent(parent);
        return point;
    }

    presenter.createToolbar = function() {
        var basicSection = presenter.createToolbarSection();
        var cursorElement = presenter.createToolbarButton(presenter.labels.cursor, presenter.CSS_CLASSES.CURSOR, presenter.CSS_CLASSES.CURSOR_IMAGE, basicSection, () =&gt; {
            presenter.setEditMode(true);
        }, ()=&gt;{});
        if (presenter.enabledFigures.point)
            presenter.createGeometricElementButton(presenter.labels.point, Point, basicSection);

        var circleSection = presenter.createToolbarSection();
        if (presenter.enabledFigures.circle)
            presenter.createGeometricElementButton(presenter.labels.circle, Circle, circleSection);
        if (presenter.enabledFigures.circleWithPoint)
            presenter.createGeometricElementButton(presenter.labels.circleWithPoint, CircleWithPoint, circleSection);
        if (presenter.enabledFigures.compasses)
            presenter.createGeometricElementButton(presenter.labels.compasses, Compasses, circleSection);
        if (presenter.enabledFigures.arcWithCenterPoint)
            presenter.createGeometricElementButton(presenter.labels.arcWithCenterPoint, ArcWithCenterPoint, circleSection);
        if (presenter.enabledFigures.angle)
            presenter.createGeometricElementButton(presenter.labels.angle, Angle, circleSection);
        if (circleSection.children().length == 0) circleSection.remove();

        var pointLineSection = presenter.createToolbarSection();
        if (presenter.enabledFigures.lineSegment)
            presenter.createGeometricElementButton(presenter.labels.lineSegment, LineSegment, pointLineSection);
        if (presenter.enabledFigures.halfOpenLineSegment)
            presenter.createGeometricElementButton(presenter.labels.halfOpenLineSegment, HalfOpenLineSegment, pointLineSection);
        if (presenter.enabledFigures.openLineSegment)
            presenter.createGeometricElementButton(presenter.labels.openLineSegment, OpenLineSegment, pointLineSection);
        if (pointLineSection.children().length == 0) pointLineSection.remove();

        cursorElement.addClass(presenter.CSS_CLASSES.SELECTED);
        presenter.setEditMode(true);

        if (!presenter.configuration.disableUndoRedoButton) {
            presenter.$undoButton.on('click', presenter.prevState);
            presenter.$redoButton.on('click', presenter.nextState);
        }
        if (!presenter.configuration.disableResetButton)
            presenter.$resetButton.on('click', presenter.resetWithoutVisibility);
        presenter.updateStateButtonsVisibility();
        if (!presenter.configuration.disableLabelToggle) {
            presenter.$labelsButton.on('click', labelsButtonHandler);
        } else {
            presenter.$labelsButton.css('visibility', 'hidden');
        }
    };

    presenter.getLabel = function(index, alphabet) {
        var result = "";
        while (index &gt;= 0) {
            var mod = index % alphabet.length;
            result = alphabet[mod] + result;
            index = Math.floor(index / alphabet.length) - 1;
        }
        return result;
    }

    presenter.createLabel = function(labelsList, alphabet) {
        var labelValue = '';
        var index = labelsList.indexOf('');
        if (index == -1) {
            labelValue = presenter.getLabel(labelsList.length, alphabet);
            labelsList.push(labelValue);
        } else {
            labelValue = presenter.getLabel(index, alphabet);
            labelsList[index] = labelValue;
        }
        return labelValue;
    }

    presenter.destroyLabel = function(labelValue, labelsList) {
        var index = labelsList.indexOf(labelValue);
        if (index == -1) return;
        labelsList[index] = '';
        if (index ===  labelsList.length - 1) {
            for (var i = labelsList.length - 1; i &gt; -1; i--) {
                if (labelsList[i] == '') {
                    labelsList.pop();
                } else {
                    break;
                }
            }
        }
    }

    presenter.finishInsert = function(abandonChanges) {
        if (abandonChanges &amp;&amp; presenter.newFigure != null) {
            presenter.newFigure.remove();
        }
        presenter.newFigure = null;
        presenter.$toolbarWrapper.find('.'+presenter.CSS_CLASSES.SELECTED).removeClass(presenter.CSS_CLASSES.SELECTED);
    };

    presenter.setEditMode = function(isEditMode) {
        presenter.editFigure = null;
        presenter.clearFigureSelection();
        presenter.isDragging = false;
        presenter.isEditMode = isEditMode;
    }

    presenter.clearFigureSelection = function() {
        for (var i = 0; i &lt; presenter.figuresList.length; i++) presenter.figuresList[i].setSelected(false);
        presenter.$removeFigure.css('display', 'none');
        presenter.$editFigure.css('display', 'none');
    }

    presenter.removePointFromFigure = function(point, parent) {
        point.removeParent(parent);
        if (!point.hasParents()) {
            if (point.isSelected || presenter.newFigure == parent) {
                point.remove();
            } else if (!point.isRoot) {
                point.setIsRoot(true);
            }
        } else if (presenter.editFigure == parent) {
            point.setSelected(false);
        }
    }

    presenter.showConfigPopup = function (title, config, acceptCallback, cancelCallback) {
        presenter.$configPopupTitle.html(title);
        presenter.$configPopupValues.html('');
        var properties = [];
        for (var i = 0; i &lt; config.length; i++) {
            var property = config[i];
            var $propertyWrapper = $('&lt;div&gt;&lt;/div&gt;');
            $propertyWrapper.addClass(presenter.CSS_CLASSES.CONFIG_PROP_WRAPPER);
            var $propertyTitle = $('&lt;div&gt;&lt;/div&gt;');
            $propertyTitle.addClass(presenter.CSS_CLASSES.CONFIG_PROP_TITLE);
            $propertyTitle.html(property.title);
            var $propertyInput = $('&lt;input&gt;&lt;/input&gt;');
            $propertyInput.addClass(presenter.CSS_CLASSES.CONFIG_PROP_INPUT);
            $propertyInput.attr('name', property.name);
            $propertyInput.attr('type', property.type);
            if (property.min !== undefined) $propertyInput.attr('min', property.min);
            $propertyInput[0].value = property.value;
            $propertyWrapper.append($propertyTitle);
            $propertyWrapper.append($propertyInput);
            presenter.$configPopupValues.append($propertyWrapper);
            properties.push({
                name: property.name,
                input: $propertyInput
            });
        }
        presenter.$configPopupAccept.off('click');
        presenter.$configPopupAccept.on('click', () =&gt; {
            var result = [];
            for (var i = 0; i &lt; properties.length; i++) {
                var property = properties[i];
                result.push({
                    name: property.name,
                    value: property.input.val() * presenter.getActualUnitLength()
                });
            }
            acceptCallback(result);
            presenter.hideConfigPopup();
        });
        presenter.$configPopupCancel.off('click');
        presenter.$configPopupCancel.on('click', () =&gt; {
            cancelCallback();
            presenter.hideConfigPopup();
        });
        presenter.$configPopup.css('display', originalConfigPopupDisplay);
    }

    presenter.hideConfigPopup = function() {
        presenter.$configPopup.css('display','none');
    }

    presenter.drawBackground = function() {
        var spacing = presenter.configuration.axisIncrement * presenter.getActualUnitLength();
        var yPosition = presenter.configuration.yAxisPosition != 0 ? presenter.configuration.yAxisPosition : Math.round(presenter.canvasWidth/2);
        var xPosition = presenter.configuration.xAxisPosition != 0 ? presenter.configuration.xAxisPosition : Math.round(presenter.canvasHeight/2);
        presenter.drawGrid(yPosition, xPosition, spacing);
        presenter.drawAxis(yPosition, xPosition, spacing, presenter.configuration.axisIncrement);
    }

    presenter.getOrCreateAxisLabel = function(value, isVertical) {
        var axis = isVertical ? 'y' : 'x';
        if (presenter.axisLabels[axis][value] != undefined) {
            return presenter.axisLabels[axis][value];
        } else {
            var $el = $("&lt;div&gt;&lt;/div&gt;");
            $el.addClass(presenter.CSS_CLASSES.AXIS_LABEL);
            $el.css('position','absolute');
            $el.html(value);
            presenter.axisLabels[axis][value] = $el;
            return $el;
        }
    }

    presenter.drawAxisLabel = function(value, isVertical, x, y) {
        var $label = presenter.getOrCreateAxisLabel(value, isVertical);
        if ($label.parent().length == 0) {
            $label.insertBefore(presenter.$canvasOverlay);
        }
        var rect = $label[0].getBoundingClientRect();
        if (isVertical) {
            y -= Math.round(rect.height/2);
        } else {
            x -= Math.round(rect.width/2);
        }
        $label.css('top', y+'px');
        $label.css('left', x+'px');
    }

    presenter.drawAxis = function(centerX, centerY, spacing, increment) {
        if (!presenter.configuration.hideYAxis &amp;&amp; 0 &lt;= centerX &amp;&amp; centerX &lt; presenter.canvasWidth) {
            presenter.context.strokeStyle = presenter.configuration.axisColor;
            presenter.context.beginPath();
            presenter.context.moveTo(centerX, presenter.canvasHeight);
            presenter.context.lineTo(centerX, 0);
            presenter.context.moveTo(centerX + 5, 5);
            presenter.context.lineTo(centerX, 0);
            presenter.context.moveTo(centerX - 5, 5);
            presenter.context.lineTo(centerX, 0);
            var tmpY = centerY + spacing;
            var multiplier = 0;
            if (presenter.configuration.hideXAxis) {
                multiplier = 1;
                tmpY = centerY;
            }
            while (tmpY &lt; presenter.canvasHeight - 10) {
                multiplier -= 1;
                presenter.drawAxisLabel(increment * multiplier, true, centerX + 10, tmpY);
                presenter.context.moveTo(centerX - 5, tmpY);
                presenter.context.lineTo(centerX + 5, tmpY);
                tmpY += spacing;
            }
            tmpY = centerY - spacing;
            multiplier = 0;
            while (tmpY &gt; 0) {
                multiplier += 1;
                presenter.drawAxisLabel(increment * multiplier, true, centerX + 10, tmpY);
                presenter.context.moveTo(centerX - 5, tmpY);
                presenter.context.lineTo(centerX + 5, tmpY);
                tmpY -= spacing;
            }
            presenter.context.stroke();
            presenter.context.closePath();
        }
        if (!presenter.configuration.hideXAxis &amp;&amp; 0 &lt;= centerY &amp;&amp; centerY &lt; presenter.canvasHeight) {
            presenter.context.strokeStyle = presenter.configuration.axisColor;
            presenter.context.beginPath();
            presenter.context.moveTo(0, centerY);
            presenter.context.lineTo(presenter.canvasWidth, centerY);
            presenter.context.moveTo(presenter.canvasWidth - 5, centerY - 5);
            presenter.context.lineTo(presenter.canvasWidth, centerY);
            presenter.context.moveTo(presenter.canvasWidth - 5, centerY + 5);
            presenter.context.lineTo(presenter.canvasWidth, centerY);
            var tmpX = centerX + spacing;
            var multiplier = 0;
            if (presenter.configuration.hideYAxis) {
                multiplier = -1;
                tmpX = centerX;
            }
            while (tmpX &lt; presenter.canvasWidth) {
                multiplier += 1;
                presenter.drawAxisLabel(increment * multiplier, false, tmpX, centerY + 10);
                presenter.context.moveTo(tmpX, centerY - 5);
                presenter.context.lineTo(tmpX, centerY + 5);
                tmpX += spacing;
            }
            tmpX = centerX - spacing;
            multiplier = 0;
            while (tmpX &gt; 10) {
                multiplier -= 1;
                presenter.drawAxisLabel(increment * multiplier, false, tmpX, centerY + 10);
                presenter.context.moveTo(tmpX, centerY - 5);
                presenter.context.lineTo(tmpX, centerY + 5);
                tmpX -= spacing;
            }
            presenter.context.stroke();
            presenter.context.closePath();
        }
    }

    presenter.drawGrid = function(centerX, centerY, spacing) {
        var color = presenter.configuration.gridColor;
        if (color.trim().length == 0) return;
        presenter.context.strokeStyle = color;
        presenter.context.beginPath();
        var tmpX = centerX % spacing;
        if (centerX &lt; 0) tmpX = spacing + tmpX;
        var iter = 0;
        while (tmpX &lt; presenter.canvasWidth &amp;&amp; iter &lt; 100) {
            presenter.context.moveTo(tmpX, 0);
            presenter.context.lineTo(tmpX, presenter.canvasHeight);
            tmpX += spacing;
            iter += 1;
        }
        var tmpY = centerY % spacing;
        if (centerY &lt; 0) tmpY = spacing + tmpY;
        iter = 0;
        while (tmpY &lt; presenter.canvasHeight &amp;&amp; iter &lt; 100) {
            presenter.context.moveTo(0, tmpY);
            presenter.context.lineTo(presenter.canvasWidth, tmpY);
            tmpY += spacing;
            iter += 1;
        }
        presenter.context.stroke();
        presenter.context.closePath();
    }

    class GeometricElement {

        isEditButtonAvailable = false;

        constructor(){}

        draw() {
            throw new Error("GeometricElement.draw is abstract and has not been implemented");
        };

        append() {
            throw new Error("GeometricElement.append is abstract and has not been implemented");
        }

        remove() {
            throw new Error("GeometricElement.remove is abstract and has not been implemented");
        }

        insertClickHandler(event) {
            throw new Error("GeometricElement.insertClickHandler is abstract and has not been implemented");
        }

        insertMoveHandler(event) {
            // returns true if canvas needs to be redrawn
            throw new Error("GeometricElement.insertMoveHandler is abstract and has not been implemented");
        }

        moveHandler(event) {
            throw new Error("GeometricElement.moveHandler is abstract and has not been implemented");
        }

        isClicked(event) {
            throw new Error("GeometricElement.isClicked is abstract and has not been implemented");
        }

        addLabel() {
            throw new Error("GeometricElement.addLabel is abstract and has not been implemented");
        }

        updateLabel() {
            throw new Error("GeometricElement.updateLabel is abstract and has not been implemented");
        }

        hideLabel() {
            throw new Error("GeometricElement.hideLabel is abstract and has not been implemented");
        }

        showLabel() {
            throw new Error("GeometricElement.showLabel is abstract and has not been implemented");
        }

        removeLabel() {
            throw new Error("GeometricElement.removeLabel is abstract and has not been implemented");
        }

        getLabelValues() {
            throw new Error("GeometricElement.getLabelValues is abstract and has not been implemented");
        }

        setSelected(isSelected, event) {
            // event is optional
            throw new Error("GeometricElement.setSelected is abstract and has not been implemented");
        }

        toJSON() {
            throw new Error("GeometricElement.toJSON is abstract and has not been implemented");
        }

        loadJSON(json) {
            throw new Error("GeometricElement.loadJSON is abstract and has not been implemented");
        }
    }

    class Point extends GeometricElement {

        static TYPE = "Point";
        static LABEL_CLASS = "point_label";
        static ICON_CLASS = "point-image";
        x;
        y;
        $label;
        labelValue = "";
        isRoot = true;
        isSelected = false;
        id;
        parentFigures = [];

        constructor() {
            super();
            presenter.maxPointIndex += 1;
            this.id = presenter.maxPointIndex;
        }

        setIsRoot(isRoot) {
            this.isRoot = isRoot;
            var figuresIndex = presenter.figuresList.indexOf(this);
            if (this.isRoot &amp;&amp; figuresIndex == -1) {
                presenter.figuresList.push(this);
            } else if (!this.isRoot &amp;&amp; figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }
        }

        append() {
            if (!this.labelValue) {
                this.labelValue = presenter.createLabel(presenter.labelsList, ALPHABET);
            }
            if (presenter.pointsList.indexOf(this) == -1) {
                presenter.pointsList.push(this);
            };
            if (this.isRoot &amp;&amp; presenter.figuresList.indexOf(this) == -1) {
                presenter.figuresList.push(this);
            };
        }

        addParent(figure) {
            if (this.parentFigures.indexOf(figure) == -1) this.parentFigures.push(figure);
        }

        removeParent(figure) {
            var parentIndex = this.parentFigures.indexOf(figure);
            if (parentIndex != -1) {
                this.parentFigures.splice(parentIndex, 1);
            }
        }

        hasParents() {
            return this.parentFigures.length &gt; 0;
        }

        removeAllParents() {
            while (this.parentFigures.length &gt; 0) {
                var parent = this.parentFigures.pop();
                parent.remove();
            }
        }

        remove() {
            presenter.destroyLabel(this.labelValue, presenter.labelsList);
            this.labelValue = '';
            var figuresIndex = presenter.figuresList.indexOf(this);
            if (figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }
            this.removeLabel();
        }

        insertClickHandler(e) {
            if (presenter.newFigure == this) {
                var location = getCanvasEventLocation(e);
                this.setLocation(location.x, location.y);
                this.append();
                presenter.pushState();
                presenter.newFigure = new Point();
            }
        }

        insertMoveHandler(e) {return false;};

        setLocation(x, y) {
            this.x = x;
            this.y = y;
        }

        moveHandler(e) {
            var location = getCanvasEventLocation(e);
            this.setLocation(location.x, location.y);
            if (this.$label) this.addLabel();
        }

        isClicked(e) {
            var location = getCanvasEventLocation(e);
            return Math.abs(location.x - this.x) &lt; 10 &amp;&amp; Math.abs(location.y - this.y) &lt; 10;
        }

        draw() {
            if (this.x == null || this.y == null) return;
            Point.drawPoint(presenter.context, this.x, this.y, this.isSelected);
        }

        static drawPoint(context, x, y, isSelected) {
            context.fillStyle = presenter.configuration.fillColor;
            context.strokeStyle = presenter.configuration.strokeColor;
            context.beginPath();
            context.arc(x, y, 4, 0, 2 * Math.PI);
            context.fill();
            context.stroke();
            context.closePath();
            if (isSelected) {
                context.beginPath();
                context.arc(x, y, 8, 0, 2 * Math.PI);
                context.stroke();
                context.closePath();
            }
        }

        addLabel() {
            if (this.x == null || this.y == null) return;
            if (!this.$label) {
                this.$label = $('&lt;div&gt;&lt;/div&gt;');
                this.$label.addClass(presenter.CSS_CLASSES.LABEL);
                this.$label.addClass(Point.LABEL_CLASS);
                this.$label.css('position', 'absolute');
                this.$label.insertBefore(presenter.$canvasOverlay);
            }
            this.$label.html(this.labelValue);
            this.$label.css('top', 'calc(' + (this.y) +'px - 1em)');
            this.$label.css('left', (this.x+5)+'px');
            if (!presenter.labelsVisibility) this.hideLabel();
        };

        updateLabel() {
            this.$label.css('top', 'calc(' + (this.y) +'px - 1em)');
            this.$label.css('left', (this.x+5)+'px');
        }

        hideLabel() {
            if (this.$label) {
                this.$label.css('display', 'none');
            }
        }

        showLabel() {
            if (this.$label) {
                this.$label.css('display', '');
            }
        }

        removeLabel() {
            if (this.$label) {
                this.$label.remove();
                this.$label = null;
            }
        };

        getLabelValues() {
            var values = [];
            if (this.labelValue.length &gt; 0) values.push(this.labelValue);
            return values;
        }

        setSelected(isSelected, event) {
            this.isSelected = isSelected;
        }

        toJSON() {
            if (this.x == null || this.y == null) return null;
            return {
                type: Point.TYPE,
                state: {
                    x: this.x,
                    y: this.y,
                    labelValue: this.labelValue,
                    id: this.id
                }
            }
        }

        loadJSON(json) {
            if (json.type != Point.TYPE) return;
            this.x = json.state.x;
            this.y = json.state.y;
            this.labelValue = json.state.labelValue;
            this.id = json.state.id;
        }

    }

    class LineSegment extends GeometricElement {

        static TYPE = "LineSegment";
        static LABEL_CLASS = "line_segment_label";
        static ICON_CLASS = "line-segment-image";

        endpoints = [];
        selectedPoint;
        creationLineLocation;
        draggingDiff;
        $lengthLabel;
        lengthLabelValue;

        constructor(){
            super();
        }

        draw() {
            if (this.endpoints.length == 1 &amp;&amp; this.creationLineLocation != null) {
                presenter.context.strokeStyle = presenter.configuration.strokeColor;
                presenter.context.beginPath();
                presenter.context.moveTo(this.endpoints[0].x, this.endpoints[0].y);
                presenter.context.lineTo(this.creationLineLocation.x, this.creationLineLocation.y);
                presenter.context.stroke();
                presenter.context.closePath();
            }
            this.drawLines();
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].draw();
            this.updateLengthLabel();
        };

        drawLines() {
            if (this.endpoints.length == 2) {
                presenter.context.strokeStyle = presenter.configuration.strokeColor;
                presenter.context.beginPath();
                presenter.context.moveTo(this.endpoints[0].x, this.endpoints[0].y);
                presenter.context.lineTo(this.endpoints[1].x, this.endpoints[1].y);
                presenter.context.stroke();
                presenter.context.closePath();
            }
        }

        append() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].append();
            if (!this.lengthLabelValue) {
                this.lengthLabelValue = presenter.createLabel(presenter.lowerLabelsList, LOWER_ALPHABET);
            }
            if (presenter.figuresList.indexOf(this) == -1) {
                presenter.figuresList.push(this);
            }
        }

        remove() {
            var selectedPoint = this.selectedPoint;
            if (presenter.editFigure == this &amp;&amp; selectedPoint != null) {
                this.selectedPoint = null;
                selectedPoint.removeParent(this);
                selectedPoint.removeAllParents();
            }

            for (var i = 0; i &lt; this.endpoints.length; i++) {
                presenter.removePointFromFigure(this.endpoints[i], this);
            }

            var figuresIndex = presenter.figuresList.indexOf(this);
            if (figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }

            presenter.destroyLabel(this.lengthLabelValue, presenter.lowerLabelsList);
            this.removeLengthLabel();
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this &amp;&amp; this.endpoints.length &lt; 2) {
                this.insertEndpoint(event);
            }
            if (this.endpoints.length == 2) {
                this.addLengthLabel();
                this.creationLineLocation = null;
                presenter.pushState();
                presenter.newFigure = new LineSegment();
            }
        }

        insertMoveHandler(event) {
            if (this.endpoints.length == 1) {
                var location = getCanvasEventLocation(event);
                if (this.creationLineLocation == null || (Math.abs(location.x - this.creationLineLocation.x) &gt;= 1 || Math.abs(location.y - this.creationLineLocation.y) &gt;= 1)) {
                    this.creationLineLocation = location;
                    return true;
                }
            }
            return false;
        }

        insertEndpoint(event) {
            var point;
            var clickedPoint = presenter.getClickedPoint(event);
            if (clickedPoint != null) {
                point = clickedPoint;
            } else {
                var location = getCanvasEventLocation(event);
                point = new Point();
                point.setLocation(location.x, location.y);
            }
            point.setIsRoot(false);
            point.addParent(this);
            this.endpoints.push(point);
            this.append();
        }

        moveHandler(event) {
            if (this.selectedPoint) this.selectedPoint.moveHandler(event);
            if (this.draggingDiff) {
                var location = getCanvasEventLocation(event);
                for (var i = 0; i &lt; this.endpoints.length; i++)
                    this.endpoints[i].setLocation(location.x + this.draggingDiff[i].x, location.y + this.draggingDiff[i].y);
            }
        }

        isClicked(event) {
            for (var i = 0; i &lt; this.endpoints.length; i++) {
                if (this.endpoints[i].isClicked(event)) return true;
            }
            return this.isLineClicked(event);
        }

        isLineClicked(event) {
            if (this.endpoints.length &lt; 2) return false;
            var location = getCanvasEventLocation(event);
            var left, right, top, bottom;
            if (this.endpoints[0].x &gt; this.endpoints[1].x) {
                left = this.endpoints[1].x;
                right = this.endpoints[0].x;
            } else {
                left = this.endpoints[0].x;
                right = this.endpoints[1].x;
            }

            if (this.endpoints[0].y &gt; this.endpoints[1].y) {
                top = this.endpoints[1].y;
                bottom = this.endpoints[0].y;
            } else {
                top = this.endpoints[0].y;
                bottom = this.endpoints[1].y;
            }

            if (right - left &lt; 10) {
                right += 5;
                left -= 5;
            }

            if (bottom - top &lt; 10) {
                top -= 5;
                bottom += 5;
            }

            if (location.x &lt; left || location.x &gt; right || location.y &lt; top || location.y &gt; bottom) return false;
            return this.distanceFromPoint(location.x, location.y) &lt; 5;

        }

        distanceFromPoint(x,y) {
            if (this.endpoints.length &lt; 2) return null;
            var x1 = this.endpoints[0].x;
            var y1 = this.endpoints[0].y;
            var x2 = this.endpoints[1].x;
            var y2 = this.endpoints[1].y;
            var base = Math.pow((x2-x1),2) + Math.pow((y2-y1),2);
            if (base &lt;= 0) return null;
            return Math.abs((x2-x1)*(y-y1)-(x-x1)*(y2-y1)) / Math.sqrt(base);
        }

        addLabel() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].addLabel();
        }

        updateLabel() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].updateLabel();
        }

        hideLabel() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].hideLabel();
            this.setLengthLabelAndMeasureVisibility(false, presenter.lengthMeasuresVisibility);
        }

        showLabel() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].showLabel();
            this.setLengthLabelAndMeasureVisibility(true, presenter.lengthMeasuresVisibility);
        }

        hideLengthMeasure() {
            this.setLengthLabelAndMeasureVisibility(presenter.labelsVisibility, false);
        }

        showLengthMeasure() {
            this.setLengthLabelAndMeasureVisibility(presenter.labelsVisibility, true);
        }

        setLengthLabelAndMeasureVisibility(isLabelVisible, isMeasureVisible) {
            if (!this.$lengthLabel) return;
            if (!isLabelVisible &amp;&amp; !isMeasureVisible) {
                this.$lengthLabel.css('display', 'none');
            } else {
                this.$lengthLabel.css('display', '');
                var text = "";
                if (isLabelVisible) {
                    text = this.lengthLabelValue;
                }
                if (isMeasureVisible) {
                    var length = this.getUnitLength();
                    if (length != null) {
                        if (text.length &gt; 0) {
                            text += " = ";
                        }
                        var lengthLabelMultiplier = Math.pow(10, presenter.configuration.lengthDecimalPoint) * 1.0;
                        text += Math.round(lengthLabelMultiplier * length)/lengthLabelMultiplier;
                    }
                }
                if (text.length &gt; 0) {
                    this.$lengthLabel.html(text);
                } else {
                    this.$lengthLabel.css('display', 'none');
                }
            }
        }

        removeLabel() {
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].removeLabel();
        }

        removeLengthLabel() {
            if (this.$lengthLabel) {
                this.$lengthLabel.remove();
                this.$lengthLabel = null;
            }
        }

        getLabelValues() {
            var values = [];
            for (var i = 0; i &lt; this.endpoints.length; i++) values = values.concat(this.endpoints[i].getLabelValues());
            return values;
        }

        getLowerLabelValues() {
            var values = [];
            if (!!this.lengthLabelValue) values.push(this.lengthLabelValue);
            return values;
        }

        getLengthLabelPosition() {
            if (this.endpoints.length &lt; 2) return null;
            return {
                x: (this.endpoints[0].x + this.endpoints[1].x)/2.0,
                y: (this.endpoints[0].y + this.endpoints[1].y)/2.0
            };
        }

        getLength() {
            if (this.endpoints.length &lt; 2 || this.getClassType() != "LineSegment") return null;
            return Math.sqrt(Math.pow(this.endpoints[0].x - this.endpoints[1].x, 2) + Math.pow(this.endpoints[0].y - this.endpoints[1].y, 2));
        }

        getUnitLength() {
            var length = this.getLength();
            if (length == null) return null;
            return presenter.convertToUnit(length);
        }

        addLengthLabel() {
            if (this.endpoints.length &lt; 2) return;
            if (!this.$lengthLabel) {
                this.$lengthLabel = $('&lt;div&gt;&lt;/div&gt;');
                this.$lengthLabel.addClass(presenter.CSS_CLASSES.LABEL);
                this.$lengthLabel.addClass(LineSegment.LABEL_CLASS);
                this.$lengthLabel.css('position', 'absolute');
                this.$lengthLabel.insertBefore(presenter.$canvasOverlay);
            }
            this.setLengthLabelAndMeasureVisibility(presenter.labelsVisibility, presenter.lengthMeasuresVisibility);
            var position = this.getLengthLabelPosition();
            this.$lengthLabel.css('top', 'calc(' + position.y + 'px - 0.5em)');
            this.$lengthLabel.css('left', 'calc(' + position.x + 'px - 0.5em)');
        }

        updateLengthLabel() {
            if (this.endpoints.length &lt; 2 || !this.$lengthLabel) return;
            this.setLengthLabelAndMeasureVisibility(presenter.labelsVisibility, presenter.lengthMeasuresVisibility);
            var position = this.getLengthLabelPosition();
            this.$lengthLabel.css('top', 'calc(' + position.y + 'px - 0.5em)');
            this.$lengthLabel.css('left', 'calc(' + position.x + 'px - 0.5em)');
        }

        setSelected(isSelected, event) {
            if (this.endpoints.length == 0) return;
            for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].setSelected(false, event);
            this.selectedPoint = null;
            this.draggingDiff = null;
            if (isSelected) {
                if (this.endpoints.length == 2 &amp;&amp; this.endpoints[1].isClicked(event)) {
                    this.selectedPoint = this.endpoints[1];
                } else if (this.endpoints[0].isClicked(event)) {
                    this.selectedPoint = this.endpoints[0];
                }
                if (this.selectedPoint) {
                    this.selectedPoint.setSelected(isSelected, event);
                } else {
                    for (var i = 0; i &lt; this.endpoints.length; i++) this.endpoints[i].setSelected(isSelected, event);
                    var location = getCanvasEventLocation(event);
                    this.draggingDiff = [];
                    this.draggingDiff.push({
                        x: this.endpoints[0].x - location.x,
                        y: this.endpoints[0].y - location.y,
                    });
                    this.draggingDiff.push({
                        x: this.endpoints[1].x - location.x,
                        y: this.endpoints[1].y - location.y,
                    });
                }
            }
        }

        getClassType() {
            return LineSegment.TYPE;
        }

        toJSON() {
            if (this.endpoints.length &lt; 2) return null;
            var endpointStates = [null, null];
            for (var i = 0; i &lt; this.endpoints.length; i++) {
                endpointStates[i] = this.endpoints[i].toJSON();
                if (endpointStates[i] == null) return null;
            }
            return {
                type: this.getClassType(),
                state: {
                    endpointStates: endpointStates,
                    lengthLabelValue: !!this.lengthLabelValue ? this.lengthLabelValue : ""
                }
            }
        }

        loadJSON(json) {
            if (json.type != this.getClassType()) return;
            if (!!json.state.lengthLabelValue) this.lengthLabelValue = json.state.lengthLabelValue;
            for (var i = 0; i &lt; 2; i++) {
                if (i &lt; this.endpoints.length) {
                    this.endpoints[i].loadJSON(json.state.endpointStates[i]);
                } else {
                    var point;
                    var existingPoint = presenter.getPointByID(json.state.endpointStates[i].state.id);
                    if (existingPoint != null) {
                        point = existingPoint;
                    } else {
                        point = new Point();
                        point.setIsRoot(false);
                    }
                    point.addParent(this);
                    this.endpoints.push(point);
                    point.loadJSON(json.state.endpointStates[i]);
                }
            }
            this.addLengthLabel();
        }
    }

    class HalfOpenLineSegment extends LineSegment {

        static TYPE = "HalfOpenLineSegment";
        static LABEL_CLASS = "half_open_line_segment_label";
        static ICON_CLASS = "half-open-line-segment-image";

        drawLines() {
            super.drawLines();
            if (this.endpoints.length == 1 &amp;&amp; this.creationLineLocation != null) {
                this.drawLineExtension(this.endpoints[0], this.creationLineLocation);
            } else if (this.endpoints.length == 2) {
                this.drawLineExtension(this.endpoints[0], this.endpoints[1]);
            }
        }

        drawLineExtension(point1, point2) {
            var targetX = -1;
            var targetY = -1;
            var dx = point2.x - point1.x;
            var dy = point2.y - point1.y;
            if (dx == 0 &amp;&amp; dy == 0) return;

            var maxX = dx &gt; 0 ? presenter.canvasWidth - point2.x : point2.x;
            var y = maxX * dy / dx;
            y = dx &gt; 0 ? point2.y + y : point2.y - y;
            if (y &gt; 0 &amp;&amp; y &lt; presenter.canvasHeight) {
                targetX = dx &gt; 0 ? presenter.canvasWidth : 0;
                targetY = y;
            } else {
                var maxY = dy &gt; 0 ? presenter.canvasHeight - point2.y : point2.y;
                var x = maxY * dx / dy;
                x = dy &gt; 0 ? point2.x + x : point2.x - x;
                if (x &gt; 0 &amp;&amp; x &lt; presenter.canvasWidth) {
                    targetX = x;
                    targetY = dy &gt; 0 ? presenter.canvasHeight : 0;
                }
            }

            if (targetX &gt;= 0 &amp;&amp; targetY &gt;= 0) {
                presenter.context.strokeStyle = presenter.configuration.strokeColor;
                presenter.context.beginPath();
                presenter.context.moveTo(point2.x, point2.y);
                presenter.context.lineTo(targetX, targetY);
                presenter.context.stroke();
                presenter.context.closePath();
            }
        }

        isLineClicked(event) {
            if (this.endpoints.length &lt; 2) return false;
            var location = getCanvasEventLocation(event);
            if (this.endpoints[0].x &lt; this.endpoints[1].x) {
                if (location.x &lt; this.endpoints[0].x) return false;
            } else {
                if (location.x &gt; this.endpoints[0].x) return false;
            }
            if (this.endpoints[0].y &lt; this.endpoints[1].y) {
                if (location.y &lt; this.endpoints[0].y) return false;
            } else {
                if (location.y &gt; this.endpoints[0].y) return false;
            }
            return this.distanceFromPoint(location.x, location.y) &lt; 5;
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this &amp;&amp; this.endpoints.length &lt; 2) {
                this.insertEndpoint(event);
            }
            if (this.endpoints.length == 2) {
                this.addLengthLabel();
                presenter.pushState();
                presenter.newFigure = new HalfOpenLineSegment();
            }
        }

        getClassType() {
            return HalfOpenLineSegment.TYPE;
        }

    }

    class OpenLineSegment extends HalfOpenLineSegment {

        static TYPE = "OpenLineSegment";
        static LABEL_CLASS = "open_line_segment_label";
        static ICON_CLASS = "open-line-segment-image";

        drawLines() {
            super.drawLines();
            if (this.endpoints.length == 1 &amp;&amp; this.creationLineLocation != null) {
                this.drawLineExtension(this.creationLineLocation, this.endpoints[0]);
            } else if (this.endpoints.length == 2) {
                this.drawLineExtension(this.endpoints[1], this.endpoints[0]);
            }
        }

        isLineClicked(event) {
            if (this.endpoints.length &lt; 2) return false;
            var location = getCanvasEventLocation(event);
            return this.distanceFromPoint(location.x, location.y) &lt; 5;
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this &amp;&amp; this.endpoints.length &lt; 2) {
                this.insertEndpoint(event);
            }
            if (this.endpoints.length == 2) {
                this.addLengthLabel();
                presenter.pushState();
                presenter.newFigure = new OpenLineSegment();
            }
        }

        getClassType() {
            return OpenLineSegment.TYPE;
        }

    }

    class CircleBase extends GeometricElement {
        static TYPE = "CircleBaseSegment";
        static LABEL_CLASS = "circle_base_label";
        static ICON_CLASS = "circle-base-image";

        centerPoint;
        radius = 0;
        draggingDiff;

        constructor() {
            super();
        }

        draw() {
            if (this.centerPoint != null) {
                this.drawCircle(this.centerPoint.x, this.centerPoint.y, this.radius);
                this.centerPoint.draw();
            }
        };

        drawCircle(x, y, radius) {
            presenter.context.fillStyle = presenter.configuration.fillColor;
            presenter.context.strokeStyle = presenter.configuration.strokeColor;
            presenter.context.beginPath();
            presenter.context.arc(x, y, radius, 0, 2 * Math.PI);
            presenter.context.stroke();
            presenter.context.closePath();
        }

        append() {
            if (this.centerPoint) {
                this.centerPoint.append();
            }

            if (presenter.figuresList.indexOf(this) == -1) {
                presenter.figuresList.push(this);
            }
        }

        remove() {
            if (presenter.editFigure == this &amp;&amp; this.draggingDiff == null) {
                this.centerPoint.removeParent(this);
                this.centerPoint.removeAllParents();
            }

            if (!!this.centerPoint) {
                presenter.removePointFromFigure(this.centerPoint, this);
            }

            var figuresIndex = presenter.figuresList.indexOf(this);
            if (figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this) {
                if (this.centerPoint == null) {
                    this.insertCenterPoint(event);
                    presenter.pushState();
                    presenter.newFigure = new CircleBase();
                }
            }
        }

        insertCenterPoint(event) {
            var point;
            var clickedPoint = presenter.getClickedPoint(event);
            if (clickedPoint != null) {
                point = clickedPoint;
            } else {
                var location = getCanvasEventLocation(event);
                point = new Point();
                point.setLocation(location.x, location.y);
            }
            point.setIsRoot(false);
            point.addParent(this);
            this.centerPoint = point;
            this.append();
        }

        insertMoveHandler(event) {
            return false;
        }

        moveHandler(event) {
            if (!this.centerPoint) return;
            if (this.draggingDiff != null) {
                var location = getCanvasEventLocation(event);
                this.centerPoint.setLocation(location.x + this.draggingDiff.x, location.y + this.draggingDiff.y);
            } else {
                this.centerPoint.moveHandler(event);
            }
        }

        isClicked(event) {
            if (!this.centerPoint) return false;
            if (this.centerPoint.isClicked(event)) return true;
            return this.isRimClicked(event);
        }

        isRimClicked(event) {
            if (!this.centerPoint) return false;
            var location = getCanvasEventLocation(event);
            var distance = this.distanceFromCenter(location.x, location.y);
            return Math.abs(this.radius - distance) &lt; 5;
        }

        distanceFromCenter(x, y) {
            var diffX = this.centerPoint.x - x;
            var diffY = this.centerPoint.y - y;
            return Math.sqrt(diffX * diffX + diffY * diffY);
        }

        addLabel() {
            if (this.centerPoint) this.centerPoint.addLabel();
        }

        updateLabel() {
            if (this.centerPoint) this.centerPoint.updateLabel(event);
        }

        hideLabel() {
            if (this.centerPoint) this.centerPoint.hideLabel(event);
        }

        showLabel() {
            if (this.centerPoint) this.centerPoint.showLabel(event);
        }

        removeLabel() {
            if (this.centerPoint) this.centerPoint.removeLabel(event);
        }

        getLabelValues() {
            if (this.centerPoint) return this.centerPoint.getLabelValues(event);
            return [];
        }

        setSelected(isSelected, event) {
            if (!this.centerPoint) return;
            this.draggingDiff = null;
            this.centerPoint.setSelected(isSelected, event);
            if (isSelected &amp;&amp; !this.centerPoint.isClicked(event) &amp;&amp; !!event) {
                var location = getCanvasEventLocation(event);
                this.draggingDiff = {
                    x: this.centerPoint.x - location.x,
                    y: this.centerPoint.y - location.y
                };
            }
        }

        getClassType() {
            return CircleBase.TYPE;
        }

        toJSON() {
            if (this.centerPoint == null || this.radius == 0) return null;
            var centerPointState = this.centerPoint.toJSON();
            if (centerPointState == null) return null;

            return {
                type: this.getClassType(),
                state: {
                    centerPoint: centerPointState,
                    radius: this.radius
                }
            }
        }

        loadJSON(json) {
            if (json.type != this.getClassType()) return;
            if (this.centerPoint == null) {
                var point;
                var existingPoint = presenter.getPointByID(json.state.centerPoint.state.id);
                if (existingPoint != null) {
                    point = existingPoint;
                } else {
                    point = new Point();
                    point.setIsRoot(false);
                }
                point.addParent(this);
                this.centerPoint = point;
            }
            this.centerPoint.loadJSON(json.state.centerPoint);
            this.radius = json.state.radius;
        }
    }

    class CircleWithPoint extends CircleBase {
            static TYPE = "CircleWithPoint";
            static LABEL_CLASS = "circle_with_point_label";
            static ICON_CLASS = "circle-with-point-image";

            rimPoint;
            radius = 0;
            selectedPoint;

        setSelected(isSelected, event) {
            if (!this.centerPoint || !this.rimPoint) return;
            this.draggingDiff = null;
            this.selectedPoint = null;
            this.centerPoint.setSelected(false);
            this.rimPoint.setSelected(false);
            if (!!event) {
                if (this.centerPoint.isClicked(event)) {
                    this.centerPoint.setSelected(isSelected, event);
                    if (isSelected) this.selectedPoint = this.centerPoint;
                } else if (this.rimPoint.isClicked(event)) {
                    this.rimPoint.setSelected(isSelected, event);
                    if (isSelected) this.selectedPoint = this.rimPoint;
                } else {
                    this.centerPoint.setSelected(isSelected, event);
                    this.rimPoint.setSelected(isSelected, event);
                    var location = getCanvasEventLocation(event);
                    this.draggingDiff = {
                        x: this.centerPoint.x - location.x,
                        y: this.centerPoint.y - location.y
                    };
                }
            }
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this) {
                if (this.centerPoint == null) {
                    this.insertCenterPoint(event);
                } else if (this.rimPoint == null) {
                    this.insertRimPoint(event);
                    presenter.pushState();
                    presenter.newFigure = new CircleWithPoint();
                }
            }
        }

        insertRimPoint(event) {
            var point;
            var clickedPoint = presenter.getClickedPoint(event);
            if (clickedPoint != null) {
                point = clickedPoint;
            } else {
                var location = getCanvasEventLocation(event);
                point = new Point();
                point.setLocation(location.x, location.y);
            }
            point.setIsRoot(false);
            point.addParent(this);
            this.rimPoint = point;
            this.radius = this.distanceFromCenter(this.rimPoint.x, this.rimPoint.y);
            this.append();
        }

        insertMoveHandler(event) {
            if (this.centerPoint != null &amp;&amp; this.rimPoint == null) {
                var location = getCanvasEventLocation(event);
                var newRadius = this.distanceFromCenter(location.x, location.y);
                if (Math.abs(this.radius - newRadius) &gt;= 1) {
                    this.radius = newRadius;
                    return true;
                }
            }
            return false;
        }

        append() {
            super.append();
            if (this.rimPoint != null) {
                this.rimPoint.append();
            }
        }

        draw() {
            super.draw();
            if (this.rimPoint != null) {
                this.radius = this.distanceFromCenter(this.rimPoint.x, this.rimPoint.y);
                this.rimPoint.draw();
            }
        };

        moveHandler(event) {
            if (!this.centerPoint || !this.rimPoint) return;

            if (this.selectedPoint) {
                this.selectedPoint.moveHandler(event);
            }
            else {
                var oldX = this.centerPoint.x;
                var oldY = this.centerPoint.y;
                var location = getCanvasEventLocation(event);
                this.centerPoint.setLocation(location.x + this.draggingDiff.x, location.y + this.draggingDiff.y);
                this.rimPoint.setLocation(this.rimPoint.x + this.centerPoint.x - oldX, this.rimPoint.y + this.centerPoint.y - oldY);
            }
        }

        addLabel() {
            super.addLabel();
            if (this.rimPoint) this.rimPoint.addLabel();
        }

        updateLabel() {
            super.updateLabel();
            if (this.rimPoint) this.rimPoint.updateLabel();
        }

        hideLabel() {
            super.hideLabel();
            if (this.rimPoint) this.rimPoint.hideLabel();
        }

        showLabel() {
            super.showLabel();
            if (this.rimPoint) this.rimPoint.showLabel(event);
        }

        removeLabel() {
            super.removeLabel();
            if (this.rimPoint) this.rimPoint.removeLabel();
        }

        getLabelValues() {
            var values = super.getLabelValues();
            if (!!this.rimPoint) values = values.concat(this.rimPoint.getLabelValues());
            return values;
        }

        getClassType() {
            return CircleWithPoint.TYPE;
        }

        remove() {
            if (presenter.editFigure == this &amp;&amp; this.draggingDiff == null) {
                if (!!this.centerPoint &amp;&amp; this.centerPoint.isSelected) {
                    this.centerPoint.removeParent(this);
                    this.centerPoint.removeAllParents();
                }
                if (!!this.rimPoint &amp;&amp; this.rimPoint.isSelected) {
                    this.rimPoint.removeParent(this);
                    this.rimPoint.removeAllParents();
                }
            }

            if (!!this.centerPoint) presenter.removePointFromFigure(this.centerPoint, this);
            if (!!this.rimPoint) presenter.removePointFromFigure(this.rimPoint, this);

            var figuresIndex = presenter.figuresList.indexOf(this);
            if (figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }
        }

        toJSON() {
            if (this.rimPoint == null) return null;
            var rimPointState = this.rimPoint.toJSON();
            if (rimPointState == null) return null;

            var circleState = super.toJSON();
            circleState.state.rimPoint = rimPointState;
            return circleState;
        }

        loadJSON(json) {
            if (json.type != this.getClassType()) return;
            super.loadJSON(json);
            if (this.rimPoint == null) {
                var point;
                var existingPoint = presenter.getPointByID(json.state.rimPoint.state.id);
                if (existingPoint != null) {
                    point = existingPoint;
                } else {
                    point = new Point();
                    point.setIsRoot(false);
                }
                point.addParent(this);
                this.rimPoint = point;
            }
            this.rimPoint.loadJSON(json.state.rimPoint);
        }

    }

    class Circle extends CircleBase {
            static TYPE = "Circle";
            static LABEL_CLASS = "circle_label";
            static ICON_CLASS = "circle-image";

            isEditButtonAvailable = true;

            getClassType() {
                return Circle.TYPE;
            }

            insertClickHandler(event) {
                if (presenter.newFigure == this) {
                    if (this.centerPoint == null) {
                        var self = this;
                        presenter.showConfigPopup(presenter.labels.circlePopupTitle, [{
                            name: 'radius',
                            title: presenter.labels.radius,
                            value: '',
                            type: 'number',
                            min: 0
                        }], (result) =&gt; {
                            for (var i = 0; i &lt; result.length; i++) {
                                if (result[i].name == 'radius') {
                                    if (result[i].value &gt; 0) {
                                        self.radius = result[i].value;
                                        self.insertCenterPoint(event);
                                        presenter.redrawCanvas();
                                        presenter.pushState();
                                        presenter.newFigure = new Circle();
                                    }
                                    return;
                                }
                            }
                        }, ()=&gt;{});
                    }
                }
            }

            openEditPopup() {
                var self = this;
                presenter.showConfigPopup(presenter.labels.circlePopupTitle, [{
                    name: 'radius',
                    title: presenter.labels.radius,
                    value: presenter.convertToUnit(this.radius),
                    type: 'number',
                    min: 0
                }], (result) =&gt; {
                    for (var i = 0; i &lt; result.length; i++) {
                        if (result[i].name == 'radius') {
                            if (result[i].value &gt; 0) {
                                self.radius = result[i].value;
                                presenter.pushState();
                                presenter.redrawCanvas();
                            }
                            return;
                        }
                    }
                }, ()=&gt;{});
            }

    }

    class Compasses extends CircleBase {
            static TYPE = "Compasses";
            static LABEL_CLASS = "compasses_label";
            static ICON_CLASS = "compasses-image";

            referencePoints = [];
            tmpLocation;
            angle;
            isReady = false;

            getClassType() {
                return Compasses.TYPE;
            }

            draw() {
                if (this.radius &gt; 0 &amp;&amp; this.centerPoint == null &amp;&amp; this.tmpLocation != null &amp;&amp; presenter.newFigure == this) {
                    this.drawCircle(this.tmpLocation.x, this.tmpLocation.y, this.radius);
                } else if (this.centerPoint != null) {
                    if (this.isReady) {
                        this.drawArc();
                    } else {
                        this.drawCircle(this.centerPoint.x, this.centerPoint.y, this.radius);
                        this.drawArcCenterPoint();
                    }
                    this.centerPoint.draw();
                }
            }

            drawArc() {
                presenter.context.fillStyle = presenter.configuration.fillColor;
                presenter.context.strokeStyle = presenter.configuration.strokeColor;
                presenter.context.beginPath();
                presenter.context.arc(this.centerPoint.x, this.centerPoint.y, this.radius, this.angle - Math.PI / 8, this.angle + Math.PI / 8);
                presenter.context.stroke();
                presenter.context.closePath();
            }

            drawArcCenterPoint() {
                var x = this.centerPoint.x + this.radius * Math.cos(this.angle);
                var y = this.centerPoint.y + this.radius * Math.sin(this.angle);
                Point.drawPoint(presenter.context, x, y, false);
            }

            isArcClicked(event) {
                if (!this.isRimClicked(event)) return false;
                var location = getCanvasEventLocation(event);
                var angle = this.getAngle(location.x, location.y);
                var maxAngle = this.angle + Math.PI / 8;
                var minAngle = this.angle - Math.PI / 8;
                if (minAngle &lt; -1.0 * Math.PI &amp;&amp; 2.0 * Math.PI + minAngle &lt; angle) return true;
                if (maxAngle &gt; Math.PI &amp;&amp; -2.0 * Math.PI + maxAngle &gt; angle) return true;
                return angle &lt;= maxAngle &amp;&amp; angle &gt;= minAngle;
            }

            isClicked(event) {
                if (!this.centerPoint) return false;
                if (this.centerPoint.isClicked(event)) return true;
                return this.isArcClicked(event);
            }

            insertClickHandler(event) {
                if (this.referencePoints.length &lt; 2) {
                    var clickedPoint = presenter.getClickedPoint(event);
                    if (clickedPoint != null) {
                        if (this.referencePoints.length == 1 &amp;&amp; this.referencePoints[0] == clickedPoint) {
                            clickedPoint.setSelected(false);
                            this.referencePoints.pop();
                        }
                        clickedPoint.setSelected(true, event);
                        this.referencePoints.push(clickedPoint);
                        if (this.referencePoints.length == 2) {
                            this.radius = this.distanceBetweenPoints(this.referencePoints[0], this.referencePoints[1]);
                            this.append();
                            presenter.redrawCanvas();
                        }
                    }
                } else {
                    if (this.centerPoint == null) {
                    var clickedPoint = presenter.getClickedPoint(event);
                    if (clickedPoint != null) {
                        this.centerPoint = clickedPoint;
                    } else {
                        var location = getCanvasEventLocation(event);
                        this.centerPoint = new Point();
                        this.centerPoint.setLocation(location.x, location.y);
                    }
                    this.centerPoint.setIsRoot(false);
                    this.centerPoint.addParent(this);
                    this.centerPoint.setSelected(true);
                    this.append();
                    } else {
                        var location = getCanvasEventLocation(event);
                        this.angle = this.getAngle(location.x, location.y);
                        this.isReady = true;
                        this.centerPoint.setSelected(false);
                        for (var i = 0; i &lt; this.referencePoints.length; i++) {
                            this.referencePoints[i].setSelected(false);
                        }
                        presenter.pushState();
                        presenter.newFigure = new Compasses();
                    }
                }
            }

            insertMoveHandler(event) {
                if (this.referencePoints.length == 2) {
                    var newLocation = getCanvasEventLocation(event);
                    if (this.tmpLocation == null || Math.abs(this.tmpLocation.x - newLocation.x) &gt;= 1 || Math.abs(this.tmpLocation.y - newLocation.y) &gt;= 1) {
                        this.tmpLocation = newLocation;
                        if (this.centerPoint != null) {
                            this.angle = this.getAngle(newLocation.x, newLocation.y);
                        }
                        return true;
                    }
                }
                return false;
            }

            distanceBetweenPoints(point1, point2) {
                return Math.sqrt(Math.pow(point1.x-point2.x, 2) + Math.pow(point1.y-point2.y, 2));
            }

            getAngle(x, y) {
                if (this.centerPoint == null) return 0;
                return presenter.getAngle(this.centerPoint.x, this.centerPoint.y, x, y);
            }

            setSelected(isSelected, event) {
                if (!this.centerPoint) return;
                this.angleDiff = null;
                this.centerPoint.setSelected(isSelected, event);
                if (isSelected &amp;&amp; !this.centerPoint.isClicked(event) &amp;&amp; !!event) {
                    var location = getCanvasEventLocation(event);
                    var newAngle = this.getAngle(location.x, location.y);
                    this.angleDiff = this.angle - newAngle;
                }
            }

            moveHandler(event) {
                if (!this.centerPoint) return;
                if (this.angleDiff != null) {
                    var location = getCanvasEventLocation(event);
                    var newAngle = this.getAngle(location.x, location.y);
                    this.angle = newAngle + this.angleDiff;
                } else {
                    this.centerPoint.moveHandler(event);
                }
            }

            remove() {
                if (presenter.editFigure == this &amp;&amp; this.angleDiff == null) {
                    this.centerPoint.removeParent(this);
                    this.centerPoint.removeAllParents();
                }

                if (!!this.centerPoint) {
                    presenter.removePointFromFigure(this.centerPoint, this);
                }

                var figuresIndex = presenter.figuresList.indexOf(this);
                if (figuresIndex != -1) {
                    presenter.figuresList.splice(figuresIndex, 1);
                }
            }

            toJSON() {
                if (!this.isReady) return null;
                var circleState = super.toJSON();
                if (circleState != null) {
                    circleState.state.angle = this.angle;
                }
                return circleState;
            }

            loadJSON(json) {
                if (json.type != this.getClassType()) return;
                super.loadJSON(json);
                this.angle = json.state.angle;
                this.isReady = true;
            }

    }

    class ArcWithCenterPoint extends CircleBase {
        static TYPE = "ArcWithCenterPoint";
        static LABEL_CLASS = "arc_with_center_point_label";
        static ICON_CLASS = "arc-with-center-point-image";

        arcStartPoint;
        arcEndPoint;
        tmpEndLocation;

        getClassType() {
            return ArcWithCenterPoint.TYPE;
        }

        draw() {
            if (this.centerPoint) this.centerPoint.draw();
            if (this.arcEndPoint || this.tmpEndLocation) {
                this.drawArc();
            }
            if (this.arcStartPoint) this.arcStartPoint.draw();
            if (this.arcEndPoint) this.arcEndPoint.draw();
        }

        drawArc() {
            this.radius = this.distanceFromCenter(this.arcStartPoint.x, this.arcStartPoint.y);
            var startAngle = this.getStartAngle();
            var endAngle;
            if (this.arcEndPoint) {
                endAngle = this.getEndAngle();
            } else if (this.tmpEndLocation) {
                endAngle = this.getTmpEndAngle();
            } else {
                return;
            }
            presenter.context.fillStyle = presenter.configuration.fillColor;
            presenter.context.strokeStyle = presenter.configuration.strokeColor;
            presenter.context.beginPath();
            presenter.context.arc(this.centerPoint.x, this.centerPoint.y, this.radius, endAngle, startAngle);
            presenter.context.stroke();
            presenter.context.closePath();
        }

        getStartAngle() {
            return presenter.getAngle(this.centerPoint.x, this.centerPoint.y, this.arcStartPoint.x, this.arcStartPoint.y);
        }

        getEndAngle() {
            return presenter.getAngle(this.centerPoint.x, this.centerPoint.y, this.arcEndPoint.x, this.arcEndPoint.y);
        }

        getTmpEndAngle() {
            return presenter.getAngle(this.centerPoint.x, this.centerPoint.y, this.tmpEndLocation.x, this.tmpEndLocation.y);
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this) {
                if (this.centerPoint == null) {
                    this.insertCenterPoint(event);
                } else if (this.arcStartPoint == null) {
                    this.insertArcStartPoint(event);
                } else if (this.arcEndPoint == null) {
                    this.insertArcEndPoint(event);
                    if (this.arcEndPoint != null) {
                        presenter.pushState();
                        presenter.newFigure = new ArcWithCenterPoint();
                    }
                }
            }
        }

        insertArcStartPoint(event) {
            var point = presenter.getClickedOrCreatePoint(event);
            if (point == this.centerPoint) return;
            point.setIsRoot(false);
            point.addParent(this);
            this.arcStartPoint = point;
            this.radius = this.distanceFromCenter(this.arcStartPoint.x, this.arcStartPoint.y);
            this.append();
        }

        insertArcEndPoint(event) {
            var point = presenter.getClickedOrCreatePoint(event);
            if (point == this.centerPoint || point == this.arcStartPoint) return;
            point.setIsRoot(false);
            point.addParent(this);
            this.arcEndPoint = point;
            this.append();
        }

        append() {
            super.append();
            if (this.arcStartPoint != null) {
                this.arcStartPoint.append();
            }
            if (this.arcEndPoint != null) {
                this.arcEndPoint.append();
            }
        }

        insertMoveHandler(event) {
            if (this.centerPoint != null &amp;&amp; this.arcStartPoint != null &amp;&amp; this.arcEndPoint == null) {
                var location = getCanvasEventLocation(event);
                if (this.tmpEndLocation == null || Math.abs(this.tmpEndLocation.x - location.x) &gt; 1 || Math.abs(this.tmpEndLocation.y - location.y) &gt; 1) {
                    this.tmpEndLocation = location;
                    return true;
                }
            }
            return false;
        }

        addLabel() {
            super.addLabel();
            if (this.arcStartPoint) this.arcStartPoint.addLabel();
            if (this.arcEndPoint) this.arcEndPoint.addLabel();
        }

        updateLabel() {
            super.updateLabel();
            if (this.arcStartPoint) this.arcStartPoint.updateLabel();
            if (this.arcEndPoint) this.arcEndPoint.updateLabel();
        }

        hideLabel() {
            super.hideLabel();
            if (this.arcStartPoint) this.arcStartPoint.hideLabel();
            if (this.arcEndPoint) this.arcEndPoint.hideLabel();
        }

        showLabel() {
            super.showLabel();
            if (this.arcStartPoint) this.arcStartPoint.showLabel();
            if (this.arcEndPoint) this.arcEndPoint.showLabel();
        }

        removeLabel() {
            super.removeLabel();
            if (this.arcStartPoint) this.arcStartPoint.removeLabel();
            if (this.arcEndPoint) this.arcEndPoint.removeLabel();
        }

        remove() {
            if (presenter.editFigure == this &amp;&amp; this.draggingDiff == null) {
                if (!!this.centerPoint &amp;&amp; this.centerPoint.isSelected) {
                    this.centerPoint.removeParent(this);
                    this.centerPoint.removeAllParents();
                }
                if (!!this.arcStartPoint &amp;&amp; this.arcStartPoint.isSelected) {
                    this.arcStartPoint.removeParent(this);
                    this.arcStartPoint.removeAllParents();
                }
                if (!!this.arcEndPoint &amp;&amp; this.arcEndPoint.isSelected) {
                    this.arcEndPoint.removeParent(this);
                    this.arcEndPoint.removeAllParents();
                }
            }

            if (!!this.centerPoint) presenter.removePointFromFigure(this.centerPoint, this);
            if (!!this.arcStartPoint) presenter.removePointFromFigure(this.arcStartPoint, this);
            if (!!this.arcEndPoint) presenter.removePointFromFigure(this.arcEndPoint, this);

            var figuresIndex = presenter.figuresList.indexOf(this);
            if (figuresIndex != -1) {
                presenter.figuresList.splice(figuresIndex, 1);
            }
        }

        setSelected(isSelected, event) {
            if (!this.centerPoint || !this.arcStartPoint|| !this.arcEndPoint) return;
            this.draggingDiff = null;
            this.centerPoint.setSelected(false);
            this.arcStartPoint.setSelected(false);
            this.arcEndPoint.setSelected(false);
            if (!!event) {
                if (this.centerPoint.isClicked(event)) {
                    this.centerPoint.setSelected(isSelected, event);
                } else if (this.arcStartPoint.isClicked(event)) {
                    this.arcStartPoint.setSelected(isSelected, event);
                } else if (this.arcEndPoint.isClicked(event)) {
                   this.arcEndPoint.setSelected(isSelected, event);
                } else {
                    this.centerPoint.setSelected(isSelected, event);
                    this.arcStartPoint.setSelected(isSelected, event);
                    this.arcEndPoint.setSelected(isSelected, event);
                    var location = getCanvasEventLocation(event);
                    this.draggingDiff = {
                        x: this.centerPoint.x - location.x,
                        y: this.centerPoint.y - location.y
                    };
                }
            }
        }

        moveHandler(event) {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return;

            if (this.draggingDiff != null) {
                var oldX = this.centerPoint.x;
                var oldY = this.centerPoint.y;
                var location = getCanvasEventLocation(event);
                this.centerPoint.setLocation(location.x + this.draggingDiff.x, location.y + this.draggingDiff.y);
                this.arcStartPoint.setLocation(this.arcStartPoint.x + this.centerPoint.x - oldX, this.arcStartPoint.y + this.centerPoint.y - oldY);
                this.arcEndPoint.setLocation(this.arcEndPoint.x + this.centerPoint.x - oldX, this.arcEndPoint.y + this.centerPoint.y - oldY);
            } else if (this.centerPoint.isSelected) {
                this.centerPoint.moveHandler(event);
            } else if (this.arcStartPoint.isSelected) {
                this.arcStartPoint.moveHandler(event);
            } else if (this.arcEndPoint.isSelected) {
                this.arcEndPoint.moveHandler(event);
            }
        }

        isArcClicked(event) {
            if (!this.isRimClicked(event)) return false;
            var location = getCanvasEventLocation(event);
            var angle = presenter.getAngle(this.centerPoint.x, this.centerPoint.y, location.x, location.y);
            var startAngle = this.getStartAngle();
            var endAngle = this.getEndAngle();
            if (startAngle &gt; endAngle) {
                return endAngle &lt;= angle &amp;&amp; angle &lt;= startAngle;
            } else {
                return endAngle &lt;= angle || angle &lt;= startAngle;
            }
        }

        isClicked(event) {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return false;
            if (this.centerPoint.isClicked(event)) return true;
            if (this.arcStartPoint.isClicked(event)) return true;
            if (this.arcEndPoint.isClicked(event)) return true;
            return this.isArcClicked(event);
        }

        getLabelValues() {
            var values = super.getLabelValues();
            if (!!this.arcStartPoint) values = values.concat(this.arcStartPoint.getLabelValues());
            if (!!this.arcEndPoint) values = values.concat(this.arcEndPoint.getLabelValues());
            return values;
        }

        toJSON() {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return null;
            var circleState = super.toJSON();
            if (circleState != null) {
                circleState.state.arcStartPoint = this.arcStartPoint.toJSON();
                circleState.state.arcEndPoint = this.arcEndPoint.toJSON();
            }
            return circleState;
        }

        loadJSON(json) {
            if (json.type != this.getClassType()) return;
            super.loadJSON(json);
            this.arcStartPoint = presenter.getOrCreateChildPointByID(json.state.arcStartPoint.state.id, this);
            this.arcStartPoint.loadJSON(json.state.arcStartPoint);
            this.arcEndPoint = presenter.getOrCreateChildPointByID(json.state.arcEndPoint.state.id, this);
            this.arcEndPoint.loadJSON(json.state.arcEndPoint);
        }
    }

    class Angle extends ArcWithCenterPoint {
        static TYPE = "Angle";
        static LABEL_CLASS = "angle_label";
        static ICON_CLASS = "angle-image";
        static DEF_RADIUS = 20;

        $angleLabel = null;

        getClassType() {
            return Angle.TYPE;
        }

        drawArc() {
            this.radius = Angle.DEF_RADIUS;
            this.updateAngleLabel();
            var startAngle = this.getStartAngle();
            var endAngle;
            if (this.arcEndPoint) {
                endAngle = this.getEndAngle();
            } else if (this.tmpEndLocation) {
                endAngle = this.getTmpEndAngle();
            } else {
                return;
            }
            var endX = this.centerPoint.x + this.radius * Math.cos(endAngle);
            var endY = this.centerPoint.y + this.radius * Math.sin(endAngle);
            presenter.context.fillStyle = presenter.configuration.fillColor;
            presenter.context.strokeStyle = presenter.configuration.strokeColor;
            presenter.context.beginPath();
            presenter.context.moveTo(this.centerPoint.x, this.centerPoint.y);
            presenter.context.lineTo(endX, endY);
            presenter.context.arc(this.centerPoint.x, this.centerPoint.y, this.radius, endAngle, startAngle);
            presenter.context.lineTo(this.centerPoint.x, this.centerPoint.y);
            presenter.context.stroke();
            presenter.context.closePath();
        }

        getAngleDegreeValue() {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return null;
            var radResult = null;
            var startAngle = this.getStartAngle();
            var endAngle = this.getEndAngle();
            if (startAngle &gt; endAngle) {
                radResult = startAngle - endAngle;
            } else {
                radResult = 2*Math.PI + startAngle - endAngle;
            }
            return radResult * 180.0 / Math.PI;
        }

        addAngleLabel() {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return null;
            if (!this.$angleLabel) {
                this.$angleLabel = $('&lt;div&gt;&lt;/div&gt;');
                this.$angleLabel.addClass(presenter.CSS_CLASSES.LABEL);
                this.$angleLabel.addClass(Angle.LABEL_CLASS);
                this.$angleLabel.css('position', 'absolute');
                this.$angleLabel.insertBefore(presenter.$canvasOverlay);
            }
            if (presenter.angleMeasuresVisibility) {
                this.showAngleMeasure();
            } else {
                this.hideAngleMeasure();
            }
            var position = this.getAngleLabelPosition();
            this.$angleLabel.css('top', 'calc(' + position.y + 'px - 0.5em)');
            this.$angleLabel.css('left', 'calc(' + position.x + 'px - 0.5em)');
            if (!presenter.labelsVisibility) this.hideLabel();
        };

        updateAngleLabel() {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint || !this.$angleLabel) return null;
            if (presenter.angleMeasuresVisibility) {
                this.showAngleMeasure();
            } else {
                this.hideAngleMeasure();
            }
            var position = this.getAngleLabelPosition();
            this.$angleLabel.css('top', 'calc(' + position.y + 'px - 0.5em)');
            this.$angleLabel.css('left', 'calc(' + position.x + 'px - 0.5em)');
        }

        getAngleLabelPosition() {
            if (!this.centerPoint || !this.arcStartPoint || !this.arcEndPoint) return null;
            var resultAngle;
            var startAngle = this.getStartAngle();
            var endAngle = this.getEndAngle();
            if (startAngle &gt; endAngle) {
                resultAngle = (startAngle + endAngle) / 2;
            } else {
                resultAngle = endAngle + (2 * Math.PI + startAngle - endAngle)/2;
            }

            var x = Math.round(this.centerPoint.x + this.radius * Math.cos(resultAngle));
            var y = Math.round(this.centerPoint.y + this.radius * Math.sin(resultAngle));
            return {x:x, y:y};
        }

        setAngleLabelAndMeasureVisibility(isLabelVisible, isMeasureVisible) {
            if (!this.$angleLabel) return;
            if (!isLabelVisible &amp;&amp; !isMeasureVisible) {
                this.$angleLabel.css('display', 'none');
            } else {
                this.$angleLabel.css('display', '');
                var text = "";
                if (isLabelVisible) {
                    text = this.angleLabelValue;
                }
                if (isMeasureVisible) {
                    if (text.length &gt; 0) {
                        text += " = ";
                    }
                    var angleLabelMultiplier = Math.pow(10, presenter.configuration.angleDecimalPoint) * 1.0;
                    text += Math.round(angleLabelMultiplier * this.getAngleDegreeValue())/angleLabelMultiplier + '°';
                }
                this.$angleLabel.html(text);
            }
        }

        hideAngleMeasure() {
            this.setAngleLabelAndMeasureVisibility(presenter.labelsVisibility, false);
        }

        showAngleMeasure() {
            this.setAngleLabelAndMeasureVisibility(presenter.labelsVisibility, true);
        }
        
        hideLabel() {
            super.hideLabel();
            this.setAngleLabelAndMeasureVisibility(false, presenter.angleMeasuresVisibility);
        }
        
        showLabel() {
            super.showLabel();
            this.setAngleLabelAndMeasureVisibility(true, presenter.angleMeasuresVisibility);
        }

        isArcClicked(event) {
            if (!this.centerPoint) return false;
            var location = getCanvasEventLocation(event);
            var distance = this.distanceFromCenter(location.x, location.y);
            if (distance &gt; this.radius + 5 || distance &lt; 10) return false;
            var angle = presenter.getAngle(this.centerPoint.x, this.centerPoint.y, location.x, location.y);
            var startAngle = this.getStartAngle();
            var endAngle = this.getEndAngle();
            if (startAngle &gt; endAngle) {
                return endAngle &lt;= angle &amp;&amp; angle &lt;= startAngle;
            } else {
                return endAngle &lt;= angle || angle &lt;= startAngle;
            }
        }

        append() {
            super.append();
            if (!this.angleLabelValue) {
                this.angleLabelValue = presenter.createLabel(presenter.greekLabelsList, GREEK_ALPHABET);
            }
        }

        insertClickHandler(event) {
            if (presenter.newFigure == this) {
                if (this.arcStartPoint == null) {
                    this.insertArcStartPoint(event);
                } else if (this.centerPoint == null) {
                    this.insertCenterPoint(event);
                } else if (this.arcEndPoint == null) {
                    this.insertArcEndPoint(event);
                    if (this.arcEndPoint != null) {
                        this.addAngleLabel();
                        presenter.pushState();
                        presenter.newFigure = new Angle();
                    }
                }
            }
        }

        insertCenterPoint(event) {
            var point = presenter.getClickedOrCreatePoint(event);
            if (point == this.arcStartPoint) return;
            point.setIsRoot(false);
            point.addParent(this);
            this.centerPoint = point;
            this.append();
        }

        insertArcStartPoint(event) {
            var point = presenter.getClickedOrCreatePoint(event);
            point.setIsRoot(false);
            point.addParent(this);
            this.arcStartPoint = point;
            this.append();
        }

        remove() {
            super.remove();
            if (this.$angleLabel) {
                this.$angleLabel.remove();
                this.$angleLabel = null;
            }
            presenter.destroyLabel(this.angleLabelValue, presenter.greekLabelsList);
        }

        getGreekLabelValues() {
            var values = [];
            if (!!this.angleLabelValue) values.push(this.angleLabelValue);
            return values;
        }

        toJSON() {
            var json = super.toJSON();
            if (json == null) return null;
            json.state.angleLabelValue = !!this.angleLabelValue ? this.angleLabelValue : "";
            return json;
        }

        loadJSON(json) {
            if (json.type != this.getClassType()) return;
            this.angleLabelValue = json.state.angleLabelValue;
            super.loadJSON(json);
            this.addAngleLabel();
        }
}

    presenter.createToolbarSection = function() {
        var element = $('&lt;div&gt;&lt;/div&gt;');
        element.addClass(presenter.CSS_CLASSES.TOOLBAR_SECTION);
        presenter.$toolbarOptions.append(element);
        return element;
    }

    presenter.createGeometricElementButton = function(name, _class, parent) {
        return presenter.createToolbarButton(name, _class.TYPE, _class.ICON_CLASS, parent, () =&gt; {
            presenter.newFigure = new _class();
        },
        () =&gt; {});
    }

    presenter.createToolbarButton = function(name, type, iconClass, parent, selectedCallback, deselectedCallback) {
        var element = $('&lt;button&gt;&lt;/button&gt;');
        element.addClass(presenter.CSS_CLASSES.TOOLBAR_BUTTON);

        var iconElement = $('&lt;div&gt;&lt;/div&gt;');
        iconElement.addClass(presenter.CSS_CLASSES.TOOLBAR_BUTTON_ICON);
        iconElement.addClass(iconClass);
        element.append(iconElement);

        var labelElement = $('&lt;div&gt;&lt;/div&gt;');
        labelElement.addClass(presenter.CSS_CLASSES.TOOLBAR_BUTTON_LABEL);
        labelElement.html(name);
        element.append(labelElement);

        element.click((e) =&gt; {
            var isSelected = element.hasClass(presenter.CSS_CLASSES.SELECTED);
            presenter.finishInsert(true);
            presenter.setEditMode(false);
            if (!isSelected) {
                selectedCallback();
                element.addClass(presenter.CSS_CLASSES.SELECTED);
            } else {
                deselectedCallback();
            }
            presenter.redrawCanvas();
        });
        parent.append(element);
        presenter.toolbarButtonsDict[type] = element;
        return element;
    }

    presenter.clearFigures = function() {
        presenter.clearFigureSelection();
        while (presenter.figuresList.length &gt; 0) {
            presenter.figuresList[presenter.figuresList.length - 1].remove();
        }
        presenter.pointsList = [];
    }

    presenter.clearPrevStates = function() {
        presenter.previousStates = [];
        presenter.prevStateIndex = -1;
    }

    presenter.reset = function() {
        presenter.resetWithoutVisibility();
        if (presenter.configuration.defaultVisibility) {
            presenter.show();
        } else {
            presenter.hide();
        }
    }

    presenter.resetWithoutVisibility = function() {
        presenter.clearPrevStates();
        presenter.clearLabels();
        presenter.clearFigures();
        presenter.redrawCanvas();
        var $button = presenter.toolbarButtonsDict['Cursor'];
        if ($button &amp;&amp; !$button.hasClass(presenter.CSS_CLASSES.SELECTED)) {
            $button.click();
        }
        presenter.prevStateIndex = -1;
        presenter.previousStates = [];
        presenter.pushState();
        presenter.updateStateButtonsVisibility();
        presenter.setLabelsVisibility(presenter.configuration.labelsDefaultVisibility);
        presenter.setAngleMeasuresVisibility(presenter.configuration.angleMeasuresDefaultVisibility);
        presenter.setLengthMeasuresVisibility(presenter.configuration.lengthMeasuresDefaultVisibility);
    }

    presenter.getSelectedButtonType = function() {
        for ([type, $element] of Object.entries(presenter.toolbarButtonsDict)) {
            if ($element.hasClass(presenter.CSS_CLASSES.SELECTED)) {
                return type;
            }
        }
        return '';
    }

    presenter.getState = function() {
        var state = {
            figures: [],
            labelsList: [...presenter.labelsList],
            greekLabelsList: [...presenter.greekLabelsList],
            lowerLabelsList: [...presenter.lowerLabelsList],
            selectedButton: presenter.getSelectedButtonType(),
            visibility: presenter.isVisible(),
            maxPointIndex: presenter.maxPointIndex,
            labelsVisibility: presenter.labelsVisibility,
            angleMeasuresVisibility: presenter.angleMeasuresVisibility,
            lengthMeasuresVisibility: presenter.lengthMeasuresVisibility
        };

        var validFiguresLabels = [];
        var validFiguresGreekLabels = [];
        var validFiguresLowerLabels = [];
        for (var i = 0 ; i &lt; presenter.figuresList.length; i++) {
            var figure = presenter.figuresList[i];
            var figureState = figure.toJSON();
            var labels = figure.getLabelValues();
            var greekLabels = !!(figure["getGreekLabelValues"]) ? figure.getGreekLabelValues() : [];
            var lowerLabels = !!(figure["getLowerLabelValues"]) ? figure.getLowerLabelValues() : [];
            if (figureState != null) {
                state.figures.push(figureState);
                validFiguresLabels = validFiguresLabels.concat(labels);
                validFiguresGreekLabels = validFiguresGreekLabels.concat(greekLabels);
                validFiguresLowerLabels = validFiguresLowerLabels.concat(lowerLabels);
            } else {
                // labels of invalid figures need to be removed from the state's labels list,
                // unless they are also a part of a valid figure
                for (var j = 0; j &lt; labels.length; j++) {
                    var label = labels[j];
                    if (validFiguresLabels.indexOf(label) == -1) {
                        presenter.destroyLabel(label, state.labelsList);
                    }
                }
                for (var j = 0; j &lt; greekLabels.length; j++) {
                    var label = greekLabels[j];
                    if (validFiguresGreekLabels.indexOf(label) == -1) {
                        presenter.destroyLabel(label, state.greekLabelsList);
                    }
                }
                for (var j = 0; j &lt; lowerLabels.length; j++) {
                    var label = lowerLabels[j];
                    if (validFiguresLowerLabels.indexOf(label) == -1) {
                        presenter.destroyLabel(label, state.lowerLabelsList);
                    }
                }
            }
        }

        return JSON.stringify(state);
    }

    presenter.upgradeState = function(state) {
        if (state["greekLabelsList"] == undefined) {
            state["greekLabelsList"] = [];
        }

        if (state["angleMeasuresVisibility"] == undefined) {
            state["angleMeasuresVisibility"] = true;
        }

        if (state["lowerLabelsList"] == undefined) {
            state["lowerLabelsList"] = [];
        }
    }

    presenter.setState = function(state) {
        presenter.clearFigures();
        var parsedState = JSON.parse(state);
        presenter.upgradeState(parsedState);
        if (parsedState.visibility) {
            presenter.show();
        } else {
            presenter.hide();
        }
        presenter.setLabelsVisibility(parsedState.labelsVisibility);
        presenter.setAngleMeasuresVisibility(parsedState.angleMeasuresVisibility);
        presenter.setLengthMeasuresVisibility(parsedState.lengthMeasuresVisibility);
        presenter.loadFiguresFromState(parsedState.figures);
        presenter.labelsList = [...parsedState.labelsList];
        presenter.greekLabelsList = [...parsedState.greekLabelsList];
        presenter.lowerLabelsList = [...parsedState.lowerLabelsList];
        presenter.redrawCanvas();
        var $button = presenter.toolbarButtonsDict[parsedState.selectedButton];
        if ($button &amp;&amp; !$button.hasClass(presenter.CSS_CLASSES.SELECTED)) {
            $button.click();
        }

        presenter.prevStateIndex = -1;
        presenter.previousStates = [];
        presenter.pushState();
        presenter.updateStateButtonsVisibility();
    }

    presenter.loadFiguresFromState = function(figuresState) {
        for (var i = 0; i &lt; figuresState.length; i++) {
            var figureState = figuresState[i];
            var figure = null;
            if (figureState.type == Point.TYPE) {
                figure = new Point();
            }
            if (figureState.type == LineSegment.TYPE) {
                figure = new LineSegment();
            }
            if (figureState.type == HalfOpenLineSegment.TYPE) {
                figure = new HalfOpenLineSegment();
            }
            if (figureState.type == OpenLineSegment.TYPE) {
                figure = new OpenLineSegment();
            }
            if (figureState.type == CircleWithPoint.TYPE) {
                figure = new CircleWithPoint();
            }
            if (figureState.type == Circle.TYPE) {
                figure = new Circle();
            }
            if (figureState.type == Compasses.TYPE) {
                figure = new Compasses();
            }
            if (figureState.type == ArcWithCenterPoint.TYPE) {
                figure = new ArcWithCenterPoint();
            }
            if (figureState.type == Angle.TYPE) {
                figure = new Angle();
            }
            if (figure != null) {
                figure.loadJSON(figureState);
                figure.append();
            }
        }
    }

    presenter.pushState = function() {
        var state = {
            figures: [],
            labelsList: [...presenter.labelsList],
            greekLabelsList: [...presenter.greekLabelsList],
            lowerLabelsList: [...presenter.lowerLabelsList],
        };
        for (var i = 0 ; i &lt; presenter.figuresList.length; i++) {
            var figureState = presenter.figuresList[i].toJSON();
            if (figureState != null) {
                state.figures.push(figureState);
            }
        }
        if (presenter.prevStateIndex !== presenter.previousStates.length - 1) {
            presenter.previousStates.splice(presenter.prevStateIndex + 1 - presenter.previousStates.length);
        }

        if (presenter.previousStates.length &gt; MAX_PREV_STATES) {
            presenter.previousStates.splice(0, presenter.previousStates.length - MAX_PREV_STATES);
        }
        presenter.previousStates.push(state);
        presenter.prevStateIndex = presenter.previousStates.length - 1;
        presenter.updateStateButtonsVisibility();
    }

    presenter.prevState = function() {
        if (presenter.previousStates.length &lt; 2 || presenter.prevStateIndex &lt; 1) return;
        presenter.prevStateIndex -= 1;
        presenter.setStateByIndex(presenter.prevStateIndex);
        presenter.updateStateButtonsVisibility();
    }

    presenter.nextState = function() {
        if (presenter.prevStateIndex == presenter.previousStates.length - 1 || presenter.prevStateIndex == -1) return;
        presenter.prevStateIndex += 1;
        presenter.setStateByIndex(presenter.prevStateIndex);
        presenter.updateStateButtonsVisibility();
    }

    presenter.setStateByIndex = function(index) {
        if (presenter.previousStates.length == 0) return;
        var state = presenter.previousStates[index];

        presenter.clearLabels();
        presenter.clearFigures();
        presenter.loadFiguresFromState(state.figures);
        presenter.labelsList = [...state.labelsList];
        presenter.greekLabelsList = [...state.greekLabelsList];
        presenter.lowerLabelsList = [...state.lowerLabelsList];
        presenter.maxPointIndex = state.maxPointIndex;
        presenter.redrawCanvas();
    }

    presenter.updateStateButtonsVisibility = function() {
        presenter.updateResetButtonVisibility();
        presenter.updateUndoRedoButtonsVisibility();
    }

    presenter.updateUndoRedoButtonsVisibility = function() {
        if (presenter.prevStateIndex == -1 || presenter.previousStates.length &lt; 2
            || presenter.configuration.disableUndoRedoButton) {
            presenter.$undoButton.css('visibility', 'hidden');
            presenter.$redoButton.css('visibility', 'hidden');
            return;
        }
        presenter.$undoButton.css('visibility', '');
        presenter.$redoButton.css('visibility', '');
        if (presenter.prevStateIndex == 0) {
            presenter.$undoButton.css('visibility', 'hidden');
        } else if (presenter.prevStateIndex == presenter.previousStates.length - 1) {
            presenter.$redoButton.css('visibility', 'hidden');
        }
    }

    presenter.updateResetButtonVisibility = function() {
        if (((presenter.prevStateIndex == -1 || presenter.previousStates.length &lt; 2)
            &amp;&amp; presenter.figuresList.length == 0) || presenter.configuration.disableResetButton) {
            presenter.$resetButton.css('visibility', 'hidden');
        } else {
            presenter.$resetButton.css('visibility', '');
        }
    }

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'reset': presenter.reset,
            'prevState': presenter.prevState,
            'nextState': presenter.nextState,
            'showLabels': presenter.showLabels,
            'hideLabels': presenter.hideLabels,
            'showAngleMeasures': presenter.showAngleMeasures,
            'hideAngleMeasures': presenter.hideAngleMeasures,
            'showLengthMeasures': presenter.showLengthMeasures,
            'hideLengthMeasures': presenter.hideLengthMeasures
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;

    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Glossary" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="List of words" nameLabel="Glossary_property_list_of_words" type="list">
            <property name="ID" nameLabel="Glossary_property_id" type="string"/>
            <property isLocalized="true" name="Text" nameLabel="Glossary_property_text" type="string"/>
            <property isLocalized="true" name="Description" nameLabel="Glossary_property_description" type="html"/>
        </property>
        <property displayName="Show In Editor" name="Visible" nameLabel="Glossary_property_show_in_editor" type="boolean"/>
        <property name="Open external link in" nameLabel="Glossary_property_open_external_link_in" type="{New tab, Same tab}"/>
	</model>
<css/><view>&lt;div class="modal-dialog"&gt;&lt;/div&gt;
</view><preview>&lt;div class="modal-dialog"&gt;&lt;/div&gt;
</preview><presenter>/*
    currentScrollTop is used to get around a problem with jQuery-ui, where on opening the dialog
    it would scroll up to an incorrect position. It is used to store the correct value of scrollTop
    (before jquery-ui scrolling it up), provide it for the needs of open dialog event handler,
    and then restore the correct value of scrollTop afterwards.
 */

function AddonGlossary_create(){
    var presenter = function() {};
    presenter.$ICPage = null;
    presenter.lastReceivedEvent = null;
    presenter.isPinchZoom = false;
    presenter.isPreview = false;
    var currentScrollTop = 0;

    var playerController;
    var eventBus;

    presenter.ERROR_MESSAGES = {
        UNIQUE_ID: "Id of each word must be unique."
    };

    presenter.addTitle = function(element, title) {
        $(element).attr('title', title);
    };

    presenter.addDescription = function(element, description) {
        $(element).html(description);
    };

    presenter.updateLaTeX = function(text) {
        var div = MathJax.HTML.Element("div", {id: "MathDiv"}, [text] );
        var math = MathJax.Hub.getAllJax(div)[0];
        MathJax.Hub.Queue(["Typeset", MathJax.Hub, math]);
    };

    presenter.validateModel = function(model) {
        var validated = true;
        var idList = [];
        for(var i = 0; i &lt; model["List of words"].length; i++) {
            var id = model["List of words"][i]["ID"];
            if(idList.indexOf(id) &gt;= 0) { // check if id already exist in model
                validated = false;
                break;
            }
            idList[i] = id;
        }
        return validated;
    };

    presenter.setDisplay = function(element, display) {
        var currentDisplay = $(element).css('display') === 'block';
        if (currentDisplay != display) {
            $(element).css({
                "display":"block",
                "width":"95%",
                "height":"90%"
            });
        }
    };

    presenter.getDialogDataById = function(words, wordID) {
        for(var i = 0; i &lt; words.length; i++) {
            if(words[i].ID == wordID) {
                return {
                    title: words[i].Text,
                    description: words[i].Description
                };
            }
        }

        return undefined;
    };

    presenter.findICPage = function () {
        presenter.$ICPage = $(presenter.$view.parent('.ic_page:first')[0]);
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_popup_page:first')[0]);
        }
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_header:first')[0]);
        }
        if (presenter.$ICPage.offset() == null){
            presenter.$ICPage = $(presenter.$view.parent('.ic_footer:first')[0]);
        }
    };

    presenter.openDialogEventHandler = function(event, ui) {
        try{
            var $dialog  = $(event.target).closest('.ui-dialog');
            var isPreview = $(".gwt-DialogBox").is('.gwt-DialogBox');
            var isPopup =  $(presenter.$ICPage).is('.ic_popup_page');
            var isMarginalPage =  $(presenter.$ICPage).is('.ic_footer') || (presenter.$ICPage).is('.ic_header');

            var presentationPosition = $(presenter.$ICPage).offset();
            var presentationWidth = $(presenter.$ICPage).outerWidth();
            var presentationHeight = isMarginalPage ?  $('.ic_page').outerHeight() : $(presenter.$ICPage).outerHeight();
            var dialogWidth = $dialog.outerWidth();
            var dialogHeight = $dialog.outerHeight();
            var topWindow = window;
            if (playerController &amp;&amp; !playerController.isPlayerInCrossDomain()) topWindow = top.window;
            var windowHeight = $(topWindow).height();
            var scrollTop = currentScrollTop;
            var previewFrame = 0;
            var popupTop = 0;
            var popupLeft = 0;
            var topPosition = 0;

            if (presenter.isPreview) {
                scrollTop = $(presenter.$ICPage).scrollTop();

                if (scrollTop &gt; 0) {
                    previewFrame = $(presenter.$ICPage).parent().parent().parent().offset().top - $(".gwt-DialogBox").offset().top;
                }

                windowHeight = ($(presenter.$ICPage).parent().parent().parent().height());
                presentationPosition.top = 0;
            }

            if (isPopup) {
                scrollTop = $(presenter.$ICPage).scrollTop();
                popupTop =  presentationPosition.top;
                if ($(topWindow).scrollTop() &gt; 0) presentationPosition.top = 0;
            }

            var visibleArea = presenter.estimateVisibleArea(presentationPosition.top, presentationHeight, scrollTop, windowHeight);
            var availableHeight = visibleArea.bottom - visibleArea.top;

            if (dialogHeight &gt;= availableHeight) {
                dialogHeight = presenter.calculateReducedDialogHeight($dialog, availableHeight);
                $dialog.css({
                    height: dialogHeight + 'px'
                });
            }

            // Check if the addon needs to account for transform css
            var scaleInfo = playerController.getScaleInformation();
            if(scaleInfo.scaleY!==1.0) {
                $dialog.css('transform', scaleInfo.transform);
                $dialog.css('transform-origin', scaleInfo.transformOrigin);
            }

            if(parseFloat(window.MobileUtils.getAndroidVersion())=='4.1'){
                if (window !== window.top) {
                    var ancestorData;
                    for (var i=0; i&lt;presenter.ancestorsData.length; i++)
                    {
                        ancestorData = presenter.ancestorsData[i];
                        $(ancestorData.wnd).scrollTop(ancestorData.offset);
                    }
                    presenter.ancestorsData = undefined;
                }
            }

            if (isPopup || presenter.isPreview) {
                popupLeft = presentationPosition.left;
                topPosition = parseInt((availableHeight - dialogHeight) / 2, 10);
            }
            else {
                topPosition = parseInt(( windowHeight - dialogHeight) / 2, 10) ;
            }

            var presentationHorizontalOffset = parseInt((presentationWidth - dialogWidth) * scaleInfo.scaleY / 2, 10);
            var leftPosition = presentationPosition.left + presentationHorizontalOffset;

            // adjust top position if Player was embedded in iframe (i.e. EverTeach)
            if (window !== window.top) {
                var iframeDialogHeight = parseInt($dialog.height(), 10);
                iframeDialogHeight += DOMOperationsUtils.calculateOuterDistances(DOMOperationsUtils.getOuterDimensions($dialog)).vertical;

                //topPosition -= scrollTop;

                if (topPosition &lt; 0) {
                    topPosition = 0;
                } else if (topPosition &gt; $(window).height() - iframeDialogHeight) {
                    topPosition = $(window).height() - iframeDialogHeight;
                }
            }


            if ($(window).scrollTop() &gt; popupTop &amp;&amp; isPopup) {
                topPosition += ($(window).scrollTop() - popupTop);
            }

            $dialog.css({
                left: (leftPosition - popupLeft) + 'px',
                top: (topPosition + scrollTop + previewFrame) + 'px',
                'font-size': '18px',
                'font-family': 'Trebuchet MS, Tahoma, Verdana, Arial, sans-serif'
            });

            $dialog.find('.ui-dialog-content').css({
                color: 'black'
            });

            if(isPopup || presenter.isPreview) {
                // For Preview and Popup dialog is moved to appropriate page
                var $overlay = $(".ui-widget-overlay");
                $(presenter.$view.closest(".ui-widget-overlay")).remove();
                if (isPreview) {
                    $(".ic_page_panel").children(".ic_page").children().last().after($overlay);
                }
                else {
                    $dialog.before($overlay);
                }
            }
        }catch(e){}
    };

    presenter.closeDialogEventHandler = function() {
        // due to the inability to close the dialog, when any video is under close button
        try{
            presenter.dialog.css("maxHeight", "none");

            if (presenter.ancestorsData !== undefined) {
                var ancestorData;
                for (i=0; i&lt;presenter.ancestorsData.length; i++)
                {
                    ancestorData = presenter.ancestorsData[i];
                    $(ancestorData.wnd).scrollTop(ancestorData.offset);
                }
                presenter.ancestorsData = undefined;
            }
        }catch(e){}
    };

    presenter.show = function(id) {
        // due to event propagation player issue, it's necessary to make sure page with glossary still exist.
        var pageClass = "." + $(presenter.$ICPage).attr('class').split(' ').join('.');
        if (!$(pageClass).length &gt; 0) {
            return
        }

        var dialog = presenter.dialog;
        var dialogData = presenter.getDialogDataById(presenter.model["List of words"], id);
        // don't display dialog if glossary hasn't needed ID
        if (!dialogData) return;

        dialog.dialog("option", "title", dialogData.title);
        presenter.addDescription(dialog, dialogData.description);

        currentScrollTop = playerController.iframeScroll();

        dialog.dialog("open");

        if (!playerController.isPlayerInCrossDomain()) {
            $(top.window).scrollTop(currentScrollTop);
        }

        presenter.updateLaTeX(dialogData.description);

        var openLinkOption = presenter.model["Open external link in"];

        if (openLinkOption == "New tab" || openLinkOption == "" || openLinkOption == undefined) {
            presenter.$view.find('.modal-dialog').find('a').attr("target", "_blank");
        } else {
            presenter.$view.find('.modal-dialog').find('a').attr("target", "_self");
        }
    };

    presenter.catchScroll = function() {
        try{
            if (window.parent != window &amp;&amp; presenter.ancestorsData === undefined) {
                var current_window = window;
                presenter.ancestorsData = [];
                while (current_window != current_window.parent) {
                    presenter.ancestorsData.push({
                        wnd: current_window.parent,
                        offset: $(current_window.parent).scrollTop()
                    });
                    current_window = current_window.parent;
                }
            }
        }catch(e){}
    };

    presenter.initializeView = function(view, model) {
        presenter.model = model;
        presenter.$view = $(view);
        presenter.findICPage();
        presenter.title = "";
        presenter.description = "";
        var position = playerController.isPlayerInCrossDomain() ? window : window.top;

        var dialog = presenter.$view.find(".modal-dialog");
        dialog.dialog({
            modal: true,
            autoOpen: false,
            draggable: false,
            width: model.Width,
            minHeight: 'auto',
            resizable: false,
            focus: presenter.catchScroll,
            open: presenter.openDialogEventHandler,
            close: presenter.closeDialogEventHandler,
            position: {
                of: position
            }
        });

        var $popup = $('#icplayer').parent().find('.ic_popup');
        var dialogWidget = dialog.dialog("widget");
        var outsideView = presenter.$view;
        outsideView.css({'display': 'block',
                        'width': 0,
                        'height': 0,
                        'position': 'static'
                        });
        outsideView.append(dialogWidget);
        if ($popup.is('.ic_popup') &amp;&amp; presenter.$view.parent().is('.ic_popup_page')) {
            // Dialog must be placed in popup page
            $popup.children().last().after(outsideView);
        }
        else if ($(".gwt-DialogBox").is('.gwt-DialogBox') ) {
            // Dialog must be placed in preview page
            $(".ic_page_panel").children(".ic_page").children().last().after(outsideView);
        }
        else {
            // Dialog must be placed outside Player so that position:absolute wouldn't be suppressed by Player's overflow:hidden
            $('#icplayer').after(outsideView);
        }
        presenter.dialog = dialog;
        presenter.$view = outsideView;
    };

    presenter.calculateReducedDialogHeight = function($dialog, pageHeight) {
        var titleHeight = $dialog.find(".ui-dialog-titlebar").outerHeight();
        var padding = parseInt($dialog.css("padding-top")) + parseInt($dialog.css("padding-bottom"));

        var $content = $dialog.find('.ui-dialog-content');
        var contentPadding = parseInt($content.css('paddingTop'), 10) + parseInt($content.css('paddingBottom'), 10);
        var contentBorder = parseInt($content.css('borderTopWidth'), 10) + parseInt($content.css('borderBottomWidth'), 10);
        var contentMargin = parseInt($content.css('marginTop'), 10) + parseInt($content.css('marginBottom'), 10);

        return pageHeight - padding - titleHeight - contentPadding - contentBorder - contentMargin;
    };

    presenter.estimateVisibleArea = function(presentationTop, presentationHeight, scrollTop, windowHeight) {
        var borders = {
            top: presentationTop,
            bottom: presentationTop + presentationHeight
        };

        if (presentationTop &lt; scrollTop) {
            borders.top = scrollTop;
        }

        if (presentationTop + presentationHeight &gt; scrollTop + windowHeight) {
            borders.bottom = scrollTop + windowHeight;
        }

        return borders;
    };

    presenter.createPreview = function(view, model) {
        presenter.isPreview = true;
        var validated = presenter.validateModel(model);
        if(validated) {
            var dialog = $(view).find(".modal-dialog");
            var visible = ModelValidationUtils.validateBoolean(model["Visible"]);
            var title = model["List of words"][0]["Text"];
            var description = model["List of words"][0]["Description"];

            presenter.addTitle(dialog, title);
            presenter.addDescription(dialog, description);

            dialog.dialog({
                modal: false,
                autoOpen: false,
                zIndex : 0,
                stack: false,
                draggable: false,
                width: model.Width,
                resizable: false
            });

            var preview = dialog.dialog("widget");
            presenter.setDisplay(preview, visible);

            $(view).append(preview);
        } else {
            $(view).html(presenter.ERROR_MESSAGES["UNIQUE_ID"]);
        }
    };

    presenter.showCommand = function (params) {
        presenter.show(params[0]);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.showCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    presenter.onEventReceived = function(eventName, eventData) {
        presenter.show(eventData.word);
    };

    function areTwoFingersOnTheScreen(event) {
        return !!(event.originalEvent.touches.length &gt;= 2);
    }

    function isTap(event) {
        return presenter.lastReceivedEvent == "touchstart"
            &amp;&amp; event.type == "touchend"
            &amp;&amp; !presenter.isPinchZoom;
    }

    presenter.shouldCloseDialog = function(event) {
        if(event.type == "click" || isTap(event)) return true;

        if(areTwoFingersOnTheScreen(event)) {
            this.isPinchZoom = true;
            return false;
        }

        this.isPinchZoom = false;
        this.lastReceivedEvent = event.type;
        return false;
    };

    function bindEvents() {
        $(".ui-widget-overlay").live("click touchstart touchend touchmove", function(event){
            if(presenter.shouldCloseDialog(event)){
                presenter.dialog.dialog("close");
            }
        });
    }

    presenter.run = function(view, model){
        presenter.isPreview = false;
        presenter.initializeView(view, model);
        eventBus = playerController.getEventBus();
        eventBus.addEventListener('Definition', this);
        bindEvents();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Gradual_Show_Answer" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Is disabled" name="Is Disabled" nameLabel="Gradual_Show_Answer_property_disabled" type="boolean"/>
        <property displayName="Is hide answers" name="Is hide answers" nameLabel="Gradual_Show_Answer_property_is_hide_answers" type="boolean"/>
        <property name="worksWith" nameLabel="Gradual_Show_Answer_property_works_with" type="text"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Gradual_Show_Answer_property_speech_texts" type="staticlist">
            <property name="AnswerHasBeenShown" nameLabel="Gradual_Show_Answer_speech_text_answer_has_been_shown" type="staticrow">
                <property name="AnswerHasBeenShown" nameLabel="Gradual_Show_Answer_speech_text_answer_has_been_shown" type="string"/>
            </property>
            <property name="AnswersAreHidden" nameLabel="Gradual_Show_Answer_speech_text_answers_are_hidden" type="staticrow">
                <property name="AnswersAreHidden" nameLabel="Gradual_Show_Answer_speech_text_answers_are_hidden" type="string"/>
            </property>
            <property name="NoNewAnswerToShow" nameLabel="Gradual_Show_Answer_speech_text_no_new_answer_to_show" type="staticrow">
                <property name="NoNewAnswerToShow" nameLabel="Gradual_Show_Answer_speech_text_no_new_answer_to_show" type="string"/>
            </property>
            <property name="EditionIsBlocked" nameLabel="Gradual_Show_Answer_speech_text_edition_is_blocked" type="staticrow">
                <property name="EditionIsBlocked" nameLabel="Gradual_Show_Answer_speech_text_edition_is_blocked" type="string"/>
            </property>
            <property name="EditionIsNotBlocked" nameLabel="Gradual_Show_Answer_speech_text_edition_is_not_blocked" type="staticrow">
                <property name="EditionIsNotBlocked" nameLabel="Gradual_Show_Answer_speech_text_edition_is_not_blocked" type="string"/>
            </property>
            <property name="Disabled" nameLabel="Gradual_Show_Answer_speech_text_disabled" type="staticrow">
                <property name="Disabled" nameLabel="Gradual_Show_Answer_speech_text_disabled" type="string"/>
            </property>
        </property>
	</model>
<css>.gradual-show-answers-button {
    background: url('resources/show-answer.png') no-repeat center;
    cursor: pointer;
    text-align: center;

    width: 100%;
    height: 100%;
}

.gradual-hide-answers-button {
    background: url('resources/hide-answer.png') no-repeat center;
}

.gradual-show-answers-container {
    width: 100%;
    height: 100%;
}

.gradual-show-answers-wrapper {
    width: 100%;
    height: 100%;
}
</css><view>&lt;div class="gradual-show-answers-wrapper"&gt;
    &lt;div class="gradual-show-answers-container"&gt;
        &lt;div class="gradual-show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="gradual-show-answers-wrapper"&gt;
    &lt;div class="gradual-show-answers-container"&gt;
        &lt;div class="gradual-show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonGradual_Show_Answer_create() {
    var presenter = function () {};

    var classList = {
        HIDE_ANSWERS: "gradual-hide-answers-button",
        BUTTON: "gradual-show-answers-button",
        GRADUAL_ACTIVE: "gradual-show-answers-active"
    }

    presenter.state = {
        isVisible: true,
        isDisabled: false,
        isErrorMode: false,
        isGradualShowAnswers: false
    };

    presenter.isWCAGOn = false;
    presenter.playerController = null;
    presenter.keyboardControllerObject = null;

    presenter.DEFAULT_TTS_PHRASES = {
        ANSWER_HAS_BEEN_SHOWN: "One answer has been shown",
        ANSWERS_ARE_HIDDEN: "All answers are hidden",
        NO_NEW_ANSWER_TO_SHOW: "No new answer to show",
        EDITION_IS_BLOCKED: "Page edition is blocked",
        EDITION_IS_NOT_BLOCKED: "Page edition is not blocked",
        DISABLED: "Disabled",
    };

    presenter.validateModel = function (model) {
        var modelValidator = new ModelValidator();

        return modelValidator.validate(model, [
            ModelValidators.utils.FieldRename("Is Visible", "isVisible", ModelValidators.Boolean('isVisible')),
            ModelValidators.utils.FieldRename("Is Disabled", "isDisabled", ModelValidators.Boolean('isDisabled')),
            ModelValidators.utils.FieldRename("Is hide answers", "isHideAnswers", ModelValidators.Boolean('isHideAnswers')),
            ModelValidators.String("worksWith", {default: ""}),
            ModelValidators.DumbString('ID'),
        ]);
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeModelAddDefaultWorksWith(model);
        return presenter.upgradeModelAddSpeechTextsProperty(upgradedModel);
    }

    presenter.upgradeModelAddDefaultWorksWith = function(model){
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["worksWith"]) {
            upgradedModel["worksWith"] = '';
        }

        return upgradedModel;
    }

    presenter.upgradeModelAddSpeechTextsProperty = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["AnswerHasBeenShown"]) {
            upgradedModel["speechTexts"]["AnswerHasBeenShown"] = {AnswerHasBeenShown: ""};
        }
        if (!upgradedModel["speechTexts"]["AnswersAreHidden"]) {
            upgradedModel["speechTexts"]["AnswersAreHidden"] = {AnswersAreHidden: ""};
        }
        if (!upgradedModel["speechTexts"]["NoNewAnswerToShow"]) {
            upgradedModel["speechTexts"]["NoNewAnswerToShow"] = {NoNewAnswerToShow: ""};
        }
        if (!upgradedModel["speechTexts"]["EditionIsBlocked"]) {
            upgradedModel["speechTexts"]["EditionIsBlocked"] = {EditionIsBlocked: ""};
        }
        if (!upgradedModel["speechTexts"]["EditionIsNotBlocked"]) {
            upgradedModel["speechTexts"]["EditionIsNotBlocked"] = {EditionIsNotBlocked: ""};
        }
        if (!upgradedModel["speechTexts"]["Disabled"]) {
            upgradedModel["speechTexts"]["Disabled"] = {Disabled: ""};
        }

        return upgradedModel;
    }

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model, presenter.isPreview)
    };

    presenter.isDisabled = function () {
        return presenter.state.isDisabled;
    }

    /**
     Handle click event.

     @method clickHandler
     @return {boolean} - command associated with the click event was fully completed correctly
    */
    presenter.clickHandler = function AddonGradualShowAnswer_clickHandler(event) {
        if (event !== undefined) {
            event.stopPropagation();
        }

        if (!isClickable()) {
            return false;
        }
  
        var eventData = {
            'source': presenter.addonID,
            'value': presenter.configuration.isHideAnswers ? "HideAllAnswers" : "ShowNextAnswer",
        };
        presenter.sendEvent(eventData);
        return presenter.triggerButtonClickedEvent();
    };

    /**
     Check if button is clickable.

     @method isClickable
     @return {boolean} - true if button clickable, otherwise false
    */
    function isClickable() {
        return !(presenter.isDisabled()
            &amp;&amp; (!presenter.state.isGradualShowAnswers
                || presenter.configuration.isHideAnswers));
    }

    presenter.sendEvent = function (eventData) {
        var eventBus = presenter.playerController.getEventBus();
        eventBus.sendEvent('ValueChanged', eventData);
    }

    /**
     Trigger button clicked event.

     @method triggerButtonClickedEvent
     @return {boolean} - command associated with the event was completed correctly
    */
    presenter.triggerButtonClickedEvent = function () {
        if (presenter.playerController == null) {
            return false;
        }
        if (presenter.configuration.isHideAnswers) {
            presenter.playerController.getCommands().hideGradualAnswers();
            return true;
        } else {
            return presenter.playerController.getCommands().showNextAnswer(presenter.configuration.worksWith);
        }
    }

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.view = view;
        presenter.$view = $(view);

        const upgradedModel = presenter.upgradeModel(model);
        const validatedModel = presenter.validateModel(upgradedModel);

        if (!validatedModel.isValid) {
            return;
        }

        presenter.viewElements = {
            button: presenter.view.getElementsByClassName(classList.BUTTON)[0]
        };

        presenter.configuration = validatedModel.value;

        if (presenter.configuration.isHideAnswers) {
            presenter.viewElements.button.classList.add(classList.HIDE_ANSWERS);
        }

        if (presenter.configuration.isDisabled) {
            presenter.disable();
        }

        if (!isPreview) {
            presenter.addHandleOfMouseActions();
            presenter.addHandlingGradualShowAnswers();
        }

        presenter.setSpeechTexts(model["speechTexts"]);
        presenter.buildKeyboardController();
    }

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorCheckingMode) {
            return;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'disable': presenter.disable,
            'enable': presenter.enable,
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.state.isVisible,
            isDisabled: presenter.state.isDisabled
        })
    }

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state);
        presenter.state.isVisible = parsedState.isVisible;
        presenter.state.isDisabled = parsedState.isDisabled;

        if (presenter.state.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    }

    presenter.show = function () {
        presenter.setVisibility(true);
    }

    presenter.hide = function () {
        presenter.setVisibility(false);
    }

    presenter.setVisibility = function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.disable = function () {
        presenter.state.isDisabled = true;
    }

    presenter.enable = function () {
        presenter.state.isDisabled = false;
    }

    presenter.addHandleOfMouseActions = function () {
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            presenter.viewElements.button.addEventListener("touchend", presenter.clickHandler);
        } else {
            presenter.viewElements.button.addEventListener("click", presenter.clickHandler);
        }
    }

    presenter.addHandlingGradualShowAnswers = function () {
        var eventBus = presenter.playerController.getEventBus();
        eventBus.addEventListener("GradualShowAnswers", this);
        eventBus.addEventListener("GradualHideAnswers", this);
    }

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName === "GradualShowAnswers") {
            presenter.viewElements.button.classList.add(classList.GRADUAL_ACTIVE);
            presenter.state.isGradualShowAnswers = true;
        } else if (eventName === "GradualHideAnswers") {
            presenter.state.isGradualShowAnswers = false;
            presenter.viewElements.button.classList.remove(classList.GRADUAL_ACTIVE);
        }
    }

    presenter.isEnabledInGSAMode = function () {
        return true;
    }

    presenter.reset = function () {
        if (presenter.configuration.isDisabled) {
            presenter.disable();
        } else {
            presenter.enable();
        }
        if (presenter.configuration.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.destroy = function AddonGradual_Show_Answer__destroy() {
        presenter.playerController = null;
        presenter.viewElements.button.removeEventListener("click", presenter.clickHandler);
        presenter.viewElements.button.removeEventListener("touchend", presenter.clickHandler);
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            AnswerHasBeenShown: presenter.DEFAULT_TTS_PHRASES.ANSWER_HAS_BEEN_SHOWN,
            AnswersAreHidden: presenter.DEFAULT_TTS_PHRASES.ANSWERS_ARE_HIDDEN,
            NoNewAnswerToShow: presenter.DEFAULT_TTS_PHRASES.NO_NEW_ANSWER_TO_SHOW,
            EditionIsBlocked: presenter.DEFAULT_TTS_PHRASES.EDITION_IS_BLOCKED,
            EditionIsNotBlocked: presenter.DEFAULT_TTS_PHRASES.EDITION_IS_NOT_BLOCKED,
            Disabled: presenter.DEFAULT_TTS_PHRASES.DISABLED,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            AnswerHasBeenShown: TTSUtils.getSpeechTextProperty(
                speechTexts.AnswerHasBeenShown.AnswerHasBeenShown,
                presenter.speechTexts.AnswerHasBeenShown),
            AnswersAreHidden: TTSUtils.getSpeechTextProperty(
                speechTexts.AnswersAreHidden.AnswersAreHidden,
                presenter.speechTexts.AnswersAreHidden),
            NoNewAnswerToShow: TTSUtils.getSpeechTextProperty(
                speechTexts.NoNewAnswerToShow.NoNewAnswerToShow,
                presenter.speechTexts.NoNewAnswerToShow),
            EditionIsBlocked: TTSUtils.getSpeechTextProperty(
                speechTexts.EditionIsBlocked.EditionIsBlocked,
                presenter.speechTexts.EditionIsBlocked),
            EditionIsNotBlocked: TTSUtils.getSpeechTextProperty(
                speechTexts.EditionIsNotBlocked.EditionIsNotBlocked,
                presenter.speechTexts.EditionIsNotBlocked),
            Disabled: TTSUtils.getSpeechTextProperty(
                speechTexts.Disabled.Disabled,
                presenter.speechTexts.Disabled),
        };
    };

    presenter.keyboardController = function(keycode, isShiftDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftDown, event);
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new GradualShowAnswerKeyboardController([], 1);
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    function GradualShowAnswerKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
        this.updateMapping();
    }

    GradualShowAnswerKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    GradualShowAnswerKeyboardController.prototype.constructor = GradualShowAnswerKeyboardController;

    GradualShowAnswerKeyboardController.prototype.exitWCAGMode = function () {
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    GradualShowAnswerKeyboardController.prototype.updateMapping = function () {
        this.mapping[window.KeyboardControllerKeys.ENTER] = this.select;
        this.mapping[window.KeyboardControllerKeys.ARROW_UP] = this.preventDefaultEvent;
        this.mapping[window.KeyboardControllerKeys.ARROW_DOWN] = this.preventDefaultEvent;
        this.mapping[window.KeyboardControllerKeys.ARROW_RIGHT] = this.preventDefaultEvent;
        this.mapping[window.KeyboardControllerKeys.ARROW_LEFT] = this.preventDefaultEvent;
        this.mapping[window.KeyboardControllerKeys.SPACE] = this.preventDefaultEvent;

        // Functionality of moving to previous/next element is handled by
        // changeCurrentModule method of KeyboardNavigationController KeyDownHandler
        this.mapping[window.KeyboardControllerKeys.TAB] = this.preventDefaultEvent;
        this.shiftKeysMapping[window.KeyboardControllerKeys.TAB] = this.preventDefaultEvent;
    };

    GradualShowAnswerKeyboardController.prototype.preventDefaultEvent = function (event) {
        if (event) {
            event.preventDefault();
        }
    };

    GradualShowAnswerKeyboardController.prototype.select = function (event) {
        if (event) {
            event.preventDefault();
        }
        if (!this.isSelectEnabled) {
            return;
        }
        const fullyCompletedClickEvent = presenter.clickHandler(event);
        this.readCurrentElement(fullyCompletedClickEvent);
    };

    GradualShowAnswerKeyboardController.prototype.readCurrentElement = function (fullyCompletedClickEvent = false) {
        var textVoices = [];

        if (isClickable()) {
            this.addSpeechTextVoicesOfClickableButton(textVoices, fullyCompletedClickEvent);
        } else {
            this.addSpeechTextVoicesOfNotClickableButton(textVoices, fullyCompletedClickEvent);
        }

        presenter.speak(textVoices);
    };

    GradualShowAnswerKeyboardController.prototype.addSpeechTextVoicesOfClickableButton = function (textVoices, fullyCompletedClickEvent = false) {
        if (presenter.configuration.isHideAnswers) {
            addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.AnswersAreHidden);
            addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.EditionIsNotBlocked);
        } else {
            if (fullyCompletedClickEvent) {
                addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.AnswerHasBeenShown);
            } else {
                addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.NoNewAnswerToShow);
            }
            addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.EditionIsBlocked);
        }
    }

    GradualShowAnswerKeyboardController.prototype.addSpeechTextVoicesOfNotClickableButton = function (textVoices) {
        addSpeechTextToVoicesArray(textVoices, presenter.speechTexts.Disabled);
    }

    function addSpeechTextToVoicesArray(textVoices, message) {
        textVoices.push(window.TTSUtils.getTextVoiceObject(message));
    }

    presenter.speak = function (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }
        return null;
    };

    return presenter;
}

AddonGradual_Show_Answer_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Grid_Scene" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Rows" nameLabel="Grid_Scene_property_rows" type="string"/>
        <property name="Columns" nameLabel="Grid_Scene_property_columns" type="string"/>
        <property name="Color" nameLabel="Grid_Scene_property_color" type="string"/>
        <property name="delay" nameLabel="Grid_Scene_property_delay" type="string"/>
        <property isDefault="true" name="default_commands" nameLabel="Grid_Scene_property_default_commands" type="staticlist">
            <property displayName="setColor(Color)" name="command_setColor" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="clear()" name="command_clear" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="setCursor(x,y)" name="command_setCursor" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="mark(x,y)" name="command_mark" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="clearMark(x,y)" name="command_clearMark" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawLeft(Steps)" name="command_drawLeft" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawUp(Steps)" name="command_drawUp" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawRight(Steps)" name="command_drawRight" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawDown(Steps)" name="command_drawDown" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawLeftFrom(x,y,Steps)" name="command_drawLeftFrom" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawUpFrom(x,y,Steps)" name="command_drawUpFrom" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawRightFrom(x,y,Steps)" name="command_drawRightFrom" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
            <property displayName="drawDownFrom(x,y,Steps)" name="command_drawDownFrom" type="staticrow">
                <property name="alias" nameLabel="Grid_Scene_property_default_commands_command_alias" type="string"/>
                <property name="arguments_aliases" nameLabel="Grid_Scene_property_default_commands_arguments_alias" type="string"/>
                <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
            </property>
        </property>
        <property name="custom_commands" nameLabel="Grid_Scene_property_custom_commands" type="list">
            <property name="command_name" nameLabel="Grid_Scene_property_custom_command_name" type="string"/>
            <property name="command_arguments" nameLabel="Grid_Scene_property_custom_command_arguments" type="string"/>
            <property name="command_code" nameLabel="Grid_Scene_property_custom_command_code" type="text"/>
            <property name="is_disabled" nameLabel="Grid_Scene_property_default_commands_is_disabled" type="boolean"/>
        </property>
        <property name="json_commands" nameLabel="Grid_Scene_property_json_commands" type="text"/>
        <property name="answer" nameLabel="Grid_Scene_property_answer" type="text"/>
	</model>
<css>.grid-scene-wrapper {
    width: 100%;
    height: 100%;
}

.grid-scene-cell {
    margin: 0;
    padding: 0;
    border: none;
}

.grid-scene-cell-element {
    float: left;
    padding: 0;
    margin: 0;
    color: black;
    width: 100%;
    height: 100%;
}

.grid-scene-cell-element-wrapper {
    float: left;
    padding: 0;
    margin: 0;
    border-style: solid;
    border-color: black;
    border-width: 1px;
    -webkit-box-sizing:border-box;
    -moz-box-sizing:border-box;
    box-sizing:border-box;
}

.grid-scene-correct {
    background-color: green;
}

.grid-scene-wrong {
    background-color: red;
}
</css><view>&lt;div class="grid-scene-wrapper"&gt;
    &lt;div class="grid-scene-cell"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="grid-scene-wrapper"&gt;
    &lt;div class="grid-scene-cell"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonGrid_Scene_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;

    presenter.ERROR_CODES = {
        GS01: "Columns and rows must be a positive integer",
        GS02: "Delay have to be a positive integer",
        GS03: "Commands have to be a valid JSON string",
        GS04: "Error while getting file with commands. Check url.",
        CP01: "Custom Command must have command_arguments property",
        CP02: "Custom Command must have is_disabled property",
        CP03: "Custom Command must have command_code property",
        CP04: "Custom Command must have command_name property",
        CP05: "Custom Command must have valid JS name",
        CP06: "Custom Command arguments must have valid JS name",
        CP07: "Multiple Custom Command declared",
        AE01: "Multiple alias declaration in default commands",
        DA01: "Default Command alias must have valid JS name",
        DA02: "Default Command Arguments aliases must have valid JS names",
        BG01: "Exceeded maximum column number",
        BG02: "Exceeded maximum row number"
    };

    presenter.LABELS = {
        "command_clear": "clear",
        "command_mark": "mark",
        "command_drawLeft": "drawLeft",
        "command_drawRight": "drawRight",
        "command_drawUp": "drawUp",
        "command_drawDown": "drawDown",
        "command_drawLeftFrom": "drawLeftFrom",
        "command_drawRightFrom": "drawRightFrom",
        "command_drawUpFrom": "drawUpFrom",
        "command_drawDownFrom": "drawDownFrom",
        "command_setColor": "setColor",
        "command_setCursor": "setCursor",
        "command_clearMark": "clearMark",
        "block_mark": "mark",
        "block_x": "x",
        "block_y": "y",
        "block_clear": "clear",
        "block_steps": "steps",
        "block_drawLeft": "drawLeft",
        "block_drawRight": "drawRight",
        "block_drawUp": "drawUp",
        "block_drawDown": "drawDown",
        "block_drawLeftFrom": "drawLeftFrom",
        "block_drawRightFrom": "drawRightFrom",
        "block_drawUpFrom": "drawUpFrom",
        "block_drawDownFrom": "drawDownFrom",
        "block_setColor": "setColor",
        "block_setCursor": "setCursor",
        "block_clearMark": "clearMark"
    };

    presenter.DEFAULT_COMMANDS_TO_BLOCKS = {
        "command_clear" : 'scene_grid_clear',
        "command_mark" : 'scene_grid_mark',
        "command_drawLeft" : 'scene_grid_drawleft',
        "command_drawRight" : 'scene_grid_drawright',
        "command_drawUp" : 'scene_grid_drawup',
        "command_drawDown" : 'scene_grid_drawdown',
        "command_drawLeftFrom" : 'scene_grid_drawleftfrom',
        "command_drawRightFrom" : 'scene_grid_drawrightfrom',
        "command_drawUpFrom" : 'scene_grid_drawupfrom',
        "command_drawDownFrom" : 'scene_grid_drawdownfrom',
        "command_setColor" : 'scene_grid_setcolor',
        "command_setCursor" : 'scene_grid_setcursor',
        "command_clearMark" : 'scene_grid_clearmark'
    };

    presenter.configuration = {
        isError : true,
        isVisible : true,
        isPreview: false,
        visibleByDefault : true,
        addonID : null,
        rows : null,
        columns : null,
        color : null,
        hasDelay: false,
        isErrorMode: false,
        isSavingAnswer: false,
        delay: 0,
        queLoopTimer: null,
        commandQueue: [],
        blockLabels: {},
        commandsLabels: {},
        excludedCommands: {},
        answerCode: "",
        isShowingAnswers: false,
        isAnswer: false,
        isVisible: true
    };


    presenter.originalCommands = null;

    presenter.commandsArgs = {
        "command_clear": "",
        "command_mark": "x, y",
        "command_drawLeft": "steps",
        "command_drawRight": "steps",
        "command_drawUp": "steps",
        "command_drawDown": "steps",
        "command_drawLeftFrom": "x, y, steps",
        "command_drawRightFrom": "x, y, steps",
        "command_drawUpFrom": "x, y, steps",
        "command_drawDownFrom": "x, y, steps",
        "command_setColor": "color",
        "command_setCursor": "x, y",
        "command_clearMark": "x, y"
    };

    presenter.coloredGrid = [];
    presenter.actualCursorPosition = [1,1];

    function delayDecorator(func) {
        if (presenter.configuration.hasDelay) {
            return function () {
                presenter.configuration.commandQueue.push({
                    function: func,
                    args: arguments
                });
            }
        } else {
            return func;
        }
    }

    function applyDecorator (func) {
        return function (args) {
            return func.apply(null, args);
        };
    }

    function applyDelayDecorator (func) {
        if (presenter.configuration.hasDelay) {
            return function (args) {
                presenter.configuration.commandQueue.push({
                    function: func,
                    args: args
                });
            };
        } else {
            return function (args) {
                return func.apply(null, args);
            };
        }
    }

    function isValidName (name) {
        return ModelValidationUtils.validateJSVariableName(name.trim()).isValid;
    }

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    var gridContainerWrapper;
    var gridContainer;

    presenter.initColoredGridArray = function Grid_Addon_initColoredGridArray(rows, columns) {
        for (var rows_index = 0; rows_index &lt; rows; rows_index++) {
            presenter.coloredGrid[rows_index] = [];
            for (var columns_index = 0; columns_index &lt; columns; columns_index++) {
                presenter.coloredGrid[rows_index][columns_index] = "Empty";
            }
        }
    };

    presenter.setColoredGridArray = function Grid_Addon_set_colored_grid_array (array) {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        for (var rows_index = 0; rows_index &lt; rows; rows_index++) {
            for (var columns_index = 0; columns_index &lt; columns; columns_index++) {
                if (array[rows_index][columns_index] != "Empty") {
                    presenter.setColor(array[rows_index][columns_index]);
                    presenter.markPoint(columns_index+1, rows_index+1);
                } else {
                    presenter.resetMark(columns_index+1, rows_index+1);
                }
            }
        }

    };

    function initGrid(model) {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        presenter.initColoredGridArray(rows,columns);
        for(var row = 0; row &lt; rows; row++) {
            for(var column = 0; column &lt; columns; column++) {
                var wrapperElement = $(document.createElement('div'));
                wrapperElement.addClass('grid-scene-cell-element-wrapper');

                var selectableElement = $(document.createElement('div'));
                selectableElement.addClass('grid-scene-cell-element');
                selectableElement.attr('coordinates', (column+1)+"-"+((rows-row)));

                wrapperElement.append(selectableElement);
                gridContainer.append(wrapperElement);
            }
        }

        var gridContainerWrapperDimensions = getElementDimensions(gridContainerWrapper);
        var gridContainerWrapperDistances = calculateInnerDistance(gridContainerWrapperDimensions);

        var wrapperDimensions = getElementDimensions(gridContainerWrapper.find('.grid-scene-cell-element-wrapper:first')[0]);
        var wrapperDistances = calculateInnerDistance(wrapperDimensions);

        var elementDimensions = getElementDimensions(gridContainerWrapper.find('.grid-scene-cell-element:first')[0]);
        var elementDistances = calculateInnerDistance(elementDimensions);

        var wrapperWidth = parseInt((model.Width - gridContainerWrapperDistances.horizontal - (wrapperDistances.horizontal * columns)) / columns, 10);
        var wrapperHeight = parseInt((model.Height - gridContainerWrapperDistances.vertical - (wrapperDistances.vertical * rows)) / rows, 10);

        var elementWidth = wrapperWidth - elementDistances.horizontal;
        var elementHeight = wrapperHeight - elementDistances.vertical;

        var newContainerWrapperHeight = wrapperHeight * rows + wrapperDistances.vertical * rows;
        var newContainerWrapperWidth = wrapperWidth * columns + wrapperDistances.horizontal * columns;

        var verticalGapHeight = model.Height - newContainerWrapperHeight;
        var horizontalGapHeight = model.Width - newContainerWrapperWidth;

        gridContainerWrapper.css('height', model.Height + 'px');
        gridContainerWrapper.css('width', model.Width + 'px');
        gridContainer.css('height', model.Height + 'px');
        gridContainer.css('width', (parseInt(model.Width, 10)+parseInt(elementWidth / 2, 10)) + 'px');

        var vertical = verticalGapHeight / rows;
        var horizontal = horizontalGapHeight / columns;

        gridContainer.find(".grid-scene-cell-element-wrapper").each(function() {
            var index = $(this).index();
            var selectedRow = parseInt(index / columns, 10);

            $(this).width(wrapperWidth + horizontal + 2);
            $(this).height(wrapperHeight + vertical + 2);

            var selectableElement = $(this).find('.grid-scene-cell-element:first');

            var lineHeight = selectedRow === rows -1 ? elementHeight + verticalGapHeight : elementHeight;
            selectableElement.css('line-height', lineHeight + "px");
        });
    }

    function getElementDimensions(element) {
        element = $(element);

        return {
            border:{
                top:parseInt(element.css('border-top-width'), 10),
                bottom:parseInt(element.css('border-bottom-width'), 10),
                left:parseInt(element.css('border-left-width'), 10),
                right:parseInt(element.css('border-right-width'), 10)
            },
            margin:{
                top:parseInt(element.css('margin-top'), 10),
                bottom:parseInt(element.css('margin-bottom'), 10),
                left:parseInt(element.css('margin-left'), 10),
                right:parseInt(element.css('margin-right'), 10)
            },
            padding:{
                top:parseInt(element.css('padding-top'), 10),
                bottom:parseInt(element.css('padding-bottom'), 10),
                left:parseInt(element.css('padding-left'), 10),
                right:parseInt(element.css('padding-right'), 10)
            }
        };
    }

    function calculateInnerDistance(elementDimensions) {
        var vertical = elementDimensions.border.top + elementDimensions.border.bottom;
        vertical += elementDimensions.margin.top + elementDimensions.margin.bottom;
        vertical += elementDimensions.padding.top + elementDimensions.padding.top;

        var horizontal = elementDimensions.border.left + elementDimensions.border.right;
        horizontal += elementDimensions.margin.left + elementDimensions.margin.right;
        horizontal += elementDimensions.padding.left + elementDimensions.padding.right;

        return {
            vertical : vertical,
            horizontal : horizontal
        };
    }

    presenter.colorSquare = function (x, y){
        if (!presenter.configuration.isSavingAnswer) {
            var coordinates = x + "-" + y;
            var element = presenter.$view.find('.grid-scene-cell-element[coordinates="' + coordinates + '"]');

            element.css('background-color', presenter.configuration.color);
            element.attr('colored', 'true');
        }
    };

    presenter.resetMark = function (x, y){
        presenter.actualCursorPosition = [x,y];
        var coordinates = x+"-"+ y;
        var element = presenter.$view.find('.grid-scene-cell-element[coordinates="'+ coordinates +'"]');
        if (ModelValidationUtils.validateIntegerInRange(x, presenter.configuration.columns + 1, 1).isValid != false) {
            if (ModelValidationUtils.validateIntegerInRange(y, presenter.configuration.rows + 1, 1).isValid != false) {
                presenter.coloredGrid[y - 1][x - 1] = "Empty";
            }
        }
        element.css('background-color', '');
        element.attr('colored', 'false');
        presenter.sendMarkEvent("clearMark", x, y);
    };
    
    
    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.run = function(view, model){
        presenter.presenterLogic(view, model, false);
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.createPreview = function(view, model){
        presenter.presenterLogic(view, model, true);
    };

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.configuration.isPreview = isPreview;

        presenter.view = view;
        presenter.$view = $(view);

        presenter.configuration = $.extend({}, presenter.configuration, presenter.validateModel(model));
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }


        gridContainerWrapper = presenter.$view.find(".grid-scene-wrapper:first");
        gridContainer = gridContainerWrapper.find(".grid-scene-cell:first");

        initGrid(model);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        if (!isPreview) {
            presenter.setQueLoopTimer();
        }

        presenter.saveAnswer(isPreview);

        presenter.actualCursorPosition = [1,1];
        presenter.startCursorPosition = presenter.actualCursorPosition;
    };

    presenter.saveAnswer = function Grid_Scene_save_answer(isPreview) {
        if (!isPreview) {
            presenter.configuration.isSavingAnswer = true;
        }

        var originalDelay = presenter.configuration.hasDelay;
        presenter.configuration.hasDelay = false;
        presenter.generateOriginalCommands();

        presenter.executeCode(presenter.configuration.answerCode);
        presenter.configuration.hasDelay = originalDelay;
        presenter.generateOriginalCommands();
        presenter.configuration.answer = $.extend(true,[], presenter.coloredGrid);

        presenter.initColoredGridArray(presenter.configuration.rows, presenter.configuration.columns);
        presenter.setColor(presenter.configuration.defaultColor);
        presenter.configuration.isSavingAnswer = false;

    };

    presenter.generateOriginalCommands = function () {
        presenter.originalCommands = {
                command_clear: delayDecorator(presenter.reset),
                command_mark: delayDecorator(presenter.mark),
                command_drawLeft: delayDecorator(presenter.drawLeft),
                command_drawRight: delayDecorator(presenter.drawRight),
                command_drawUp: delayDecorator(presenter.drawUp),
                command_drawDown: delayDecorator(presenter.drawDown),
                command_drawLeftFrom: delayDecorator(presenter.drawLeft),
                command_drawRightFrom: delayDecorator(presenter.drawRight),
                command_drawUpFrom: delayDecorator(presenter.drawUp),
                command_drawDownFrom: delayDecorator(presenter.drawDown),
                command_setColor: delayDecorator(presenter.setColor),
                command_setCursor: delayDecorator(presenter.setCursor),
                command_clearMark: delayDecorator(presenter.resetMark)
        };
    };

    presenter.setQueLoopTimer = function () {
        if(presenter.configuration.hasDelay) {
            presenter.configuration.queLoopTimer = setInterval(presenter.queLoop, presenter.configuration.delay)
        }
    };

    presenter.queLoop = function () {
        if (presenter.configuration.commandQueue.length &gt; 0) {
            var task = presenter.configuration.commandQueue.shift();
            task.function.apply(null, task.args);
        }
    };
    presenter.destroy = function () {
        clearInterval(presenter.configuration.queLoopTimer);
        presenter.$view = null;
        presenter.view = null;
        presenter.configuration = null;
        presenter.originalCommands = null;
        presenter.commandsArgs = null;
        presenter.coloredGrid = null;
        presenter.actualCursorPosition = null;
        presenter.lastState = null;
    };


    function haveDuplicatedValue(firstDict, secondDict) {
        for (var key in firstDict) {
            for (var comparedKey in secondDict) {
                if (firstDict.hasOwnProperty(key) &amp;&amp; secondDict.hasOwnProperty(comparedKey)) {
                    if (key != comparedKey) {
                        if (firstDict[key] == secondDict[comparedKey]) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }

    presenter.validateModel = function(model) {
        var validatedIsVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        var addonID = model['ID'];
        var rows = ModelValidationUtils.validatePositiveInteger(model['Rows']);
        if (rows.value &gt; 40) {
            return {
                isValid: false,
                errorCode: "BG02"
            }
        }
        var columns = ModelValidationUtils.validatePositiveInteger(model['Columns']);
        if (columns.value &gt; 40) {
            return {
                isValid: false,
                errorCode: "BG01"
            }
        }

        if(!rows.isValid || !columns.isValid){
            return returnErrorObject('GS01');
        }

        var color = model['Color'];
        if(color == ''){
            color = 'black';
        }

        var validatedDelay = presenter.validateDelay(model);
        if (!validatedDelay.isValid) {
            return validatedDelay;
        }


        var validatedDefaultCommands = presenter.validateDefaultCommands(model);
        if (!validatedDefaultCommands.isValid) {
            return validatedDefaultCommands;
        }

        var validatedTranslations = validatedDefaultCommands.value.translations;
        if (haveDuplicatedValue(validatedTranslations, validatedTranslations)) {
            return presenter.getErrorObject("AE01");
        }

        presenter.commandsArgs = $.extend(presenter.commandsArgs, validatedDefaultCommands.value.argumentsTranslation);

        var validatedCustomCommands = presenter.validateCustomCommands(model['custom_commands']);
        if (!validatedCustomCommands.isValid) {
            return validatedCustomCommands;
        }

        var validatedCommandsFromFile = presenter.validateCommandsJSON(model['json_commands']);
        if (!validatedCommandsFromFile.isValid) {
            return validatedCommandsFromFile;
        }

        var mergedCustomCommands = $.merge(validatedCustomCommands.value.commands,
                                 validatedCommandsFromFile.value.commands);

        var customCommandsNames = {};
        for (var i = 0; i &lt; mergedCustomCommands.length; i++) {
            customCommandsNames[i] = mergedCustomCommands[i].name;
        }

        if (haveDuplicatedValue(customCommandsNames, customCommandsNames)) {
            return presenter.getErrorObject("CP07");
        }

        return {
            'isError' : false,
            'isVisible' : validatedIsVisible,
            'visibleByDefault' : validatedIsVisible,
            'addonID' : addonID,
            'rows' : rows.value,
            'columns' : columns.value,
            'color' : color,
            'defaultColor': color,
            'startPoint': null,
            'hasDelay': validatedDelay.hasDelay,
            'delay': validatedDelay.delay,
            'labels': validatedTranslations,
            'customCommands': mergedCustomCommands,
            'excludedCommands': $.extend({},validatedCustomCommands.value.excludedCommands,
                                    validatedDefaultCommands.value.excludedCommands,
                                    validatedCommandsFromFile.value.excludedCommands),
            'answerCode': model['answer'],
            'isAnswer': !ModelValidationUtils.isStringEmpty(model['answer'])
        };
    };

    presenter.validateDefaultCommand = function Grid_Scene_validate_default_command (key, command) {
        if (!ModelValidationUtils.isStringEmpty(command['alias'].trim())) {
            if (!isValidName(command['alias'])) {
                return presenter.getErrorObject("DA01");
            }
        }
        return {
            isValid: true,
            value : {
                validatedTranslation: command['alias'],
                validatedIsExcluded: ModelValidationUtils.validateBoolean(command['is_disabled']),
                validatedArgumentsTranslation: command['arguments_aliases']
            }
        };
    };

    presenter.validateDefaultCommands = function Grid_Scene_validate_default_commands (model) {
        var translations = {};
        var excludedCommands = {};
        var argumentsTranslation = {};
        var defaultCommands = model['default_commands'];
        for (var key in defaultCommands) {
            if (defaultCommands.hasOwnProperty(key)) {
                var validatedDefaultCommand = presenter.validateDefaultCommand(key, defaultCommands[key]);
                if (!validatedDefaultCommand.isValid) {
                    return validatedDefaultCommand;
                }
                if (!ModelValidationUtils.isStringEmpty(validatedDefaultCommand.value.validatedTranslation)) {
                    translations[key] = validatedDefaultCommand.value.validatedTranslation;
                }
                if (validatedDefaultCommand.value.validatedIsExcluded) {
                    excludedCommands[key] = true;
                }
                if (!ModelValidationUtils.isStringEmpty(validatedDefaultCommand.value.validatedArgumentsTranslation)) {
                    argumentsTranslation[key] = validatedDefaultCommand.value.validatedArgumentsTranslation;
                }
            }
    }

        return {
            isValid: true,
            value: {
                translations: translations,
                excludedCommands: excludedCommands,
                argumentsTranslation: argumentsTranslation
            }
        }
    };

    presenter.validateCustomCommand = function Grid_Scene_validate_command (command) {

        if (!command.hasOwnProperty('command_arguments')) {
            return presenter.getErrorObject("CP01");
        }
        if (!command.hasOwnProperty('is_disabled')) {
            return presenter.getErrorObject("CP02");
        }
        if (!command.hasOwnProperty('command_code')) {
            return presenter.getErrorObject("CP03");
        }
        if (!command.hasOwnProperty('command_name')) {
            return presenter.getErrorObject("CP04");
        }

        if (ModelValidationUtils.isStringEmpty(command['command_name'])) {
            return {
                isValid: true,
                name: null
            };
        }

        if (!isValidName(command['command_name'])) {   //REGEX to check name
            return presenter.getErrorObject("CP05");
        }

        if (!ModelValidationUtils.isStringEmpty(command['command_arguments'].trim())) {
            var argumentsSplited = command['command_arguments'].split(",");
            for (var key in argumentsSplited) {
                if(argumentsSplited.hasOwnProperty(key)) {
                    if(!isValidName(argumentsSplited[key])) { //REGEX to check name
                        return presenter.getErrorObject("CP06");
                    }
                }
            }
        }

        return {
            isValid: true,
            name: command['command_name'],
            arguments: command['command_arguments'],
            code: command['command_code'],
            isExcluded: ModelValidationUtils.validateBoolean(command['is_disabled'])
        };
    };

    presenter.validateCustomCommands = function Grid_Scene_validate_commands (commands) {
        var validatedCommands = [];
        var excludedCommands = {};

        for (var key in commands) {
            if (commands.hasOwnProperty(key)) {
                var validatedCommand = presenter.validateCustomCommand(commands[key]);
                if (!validatedCommand.isValid) {
                    return validatedCommand;
                }

                if (validatedCommand.name != null) {
                    validatedCommands.push(validatedCommand);
                    if (validatedCommand.isExcluded) {
                        excludedCommands[validatedCommand.name] = validatedCommand.isExcluded;
                    }
                }
            }
        }

        return {
            isValid: true,
            value: {
                commands: validatedCommands,
                excludedCommands: excludedCommands
            }
        };
    };

    presenter.validateCommandsJSON = function Grid_Scene_validate_commands_file (commands) {
        if (commands === undefined) {
            return {};
        }

        var trimmedCommands = commands.trim();
        var result;

        if (trimmedCommands == "") {
            result = {};
        } else {
            /*
            Remove all new lines from text
            e.g. : "Something
                    Here" -&gt; "SomethingHere"
             */
            var data = commands.replace(/\r?\n|\r/g,""); //removing all new lines
            /*
            Change all tabulators to space in text
            e.g. : "Something    Here" -&gt; "Something here"
             */
            data = data.replace(/\t/g," ");

            try {
                result = JSON.parse(data);
            } catch (e) {
                return  presenter.getErrorObject("GS03");
            }
        }

        var validatedCustomCommands = presenter.validateCustomCommands(result);
        if (!validatedCustomCommands.isValid) {
            return validatedCustomCommands;
        }

        return {
            isValid: true,
            value: validatedCustomCommands.value
        };
    };



    presenter.validateDelay = function(model) {
        function getDelayObject (isValid, hasDelay, delay) {return {isValid: isValid, hasDelay: hasDelay, delay: delay};}

        if (model["delay"] == undefined) {
            return getDelayObject(true, false);
        }

        var trimmedDelay = model["delay"].trim();
        if (trimmedDelay == "") {
            return getDelayObject(true, false);
        }

        var parsedDelay = Number(trimmedDelay);
        if(isNaN(parsedDelay)) {
            return presenter.getErrorObject("GS02");
        }

        if (parsedDelay &gt; 0) {
            return getDelayObject(true, true, parsedDelay);
        } else {
            return getDelayObject(true, false, parsedDelay);
        }
    };

    presenter.getErrorObject = function (errorCode) {
        return {isValid: false, isError: true, errorCode: errorCode};
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.configuration.visibility = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.getDefaultCommands = function Grid_Scene_get_commands (withParams) {
        var functions = "";
        var labels = presenter.configuration.labels;

        if (withParams === undefined) {
            withParams = false;
        }

        for (var key in presenter.originalCommands) {
            if (presenter.originalCommands.hasOwnProperty(key)) {
                if (!(presenter.configuration.excludedCommands.hasOwnProperty(key))) {
                    var functionText = "";
                    var args = "";

                    if (labels.hasOwnProperty(key)) {
                        functionText = labels[key];
                    } else {
                        functionText = presenter.LABELS[key];
                    }

                    if (withParams) {
                        args = "(" + presenter.commandsArgs[key] + ")";
                    }

                    functions += functionText + args + "&lt;br /&gt;";
                }
            }
        }
        return functions;
    };

    presenter.getCustomCommands = function Grid_Screne_get_custom_commands (withParams) {
        var commands = "";

        if (withParams === undefined) {
            withParams = false;
        }

        var customCommands = presenter.configuration.customCommands;
        for (var key in customCommands) {
            if (customCommands.hasOwnProperty(key)) {
                if (!(customCommands[key].name in presenter.configuration.excludedCommands)) {
                    var args ="";

                    if (withParams) {
                        args = "(" + customCommands[key].arguments + ")";
                    }

                    commands += customCommands[key].name + args + "&lt;br /&gt;";
                }
            }
        }
        return commands;
    };

    presenter.getCommands = function Grid_Screne_get_custom_commands (withParams) {
        if (withParams === undefined) {
            withParams = false;
        }

        return presenter.getDefaultCommands(withParams) + presenter.getCustomCommands(withParams);
    };


    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'mark' : applyDelayDecorator(presenter.mark),
            'drawLeft': applyDelayDecorator(presenter.drawLeft),
            'drawRight': applyDelayDecorator(presenter.drawRight),
            'drawDown': applyDelayDecorator(presenter.drawDown),
            'drawUp': applyDelayDecorator(presenter.drawUp),
            'drawLeftFrom':applyDelayDecorator(presenter.drawLeft),
            'drawRightFrom': applyDelayDecorator(presenter.drawRight),
            'drawDownFrom': applyDelayDecorator(presenter.drawDown),
            'drawUpFrom': applyDelayDecorator(presenter.drawUp),
            'setCursor': applyDelayDecorator(presenter.setCursor),
            'setColor': applyDelayDecorator(presenter.setColor),
            'clearMark' : applyDelayDecorator(presenter.resetMark),
            'clear': applyDelayDecorator(presenter.reset),
            'reset' : presenter.reset,
            'executeCode': applyDecorator(presenter.executeCode),
            'getDefaultCommands': applyDelayDecorator(presenter.getDefaultCommands),
            'getCustomCommands': applyDelayDecorator(presenter.getCustomCommands),
            'getCommands': applyDelayDecorator(presenter.getCommands),
            'isAllOK': presenter.isAllOK
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.reset = function(){
        presenter.$view.find('.grid-scene-cell-element').each(function () {
            $(this).removeClass('grid-scene-wrong').removeClass('grid-scene-cell-element-wrapper');
            if($(this).attr('colored') == 'true'){
                var coordinates = $(this).attr('coordinates').split('-');
                presenter.resetMark(coordinates[0], coordinates[1]);
            }
        });

        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        presenter.initColoredGridArray(rows,columns);

        presenter.setVisibility(presenter.configuration.visibleByDefault);
        presenter.configuration.color = presenter.configuration.defaultColor;

        if(presenter.configuration.answer.length &gt; 0){
            presenter.actualCursorPosition = presenter.startCursorPosition;
        }else {
            presenter.actualCursorPosition = [1,1];
        }

        presenter.configuration.isErrorMode = false;
        presenter.configuration.isShowingAnswers = false;
    };

    presenter.setCursor = function (x, y) {
        if(!isNaN(parseInt(x, 10)) &amp;&amp; !isNaN(parseInt(y, 10))) {
            presenter.actualCursorPosition = [x, y];
        }
    };

    presenter.drawHorizontalLine = function (from, to, y) {
        if (from &lt;= to) {
            for (var i = from; i &lt;= to; i++) {
                presenter.markPoint(i, y);
            }
        } else {
            for (var i = from; i &gt;= to; i--) {
                presenter.markPoint(i, y);
            }
        }
    };

    presenter.drawVerticalLine = function (from, to, x) {
        if (from &lt;= to) {
            for (var i = from; i &lt;= to; i++) {
                presenter.markPoint(x, i);
            }
        } else {
            for (var i = from; i &gt;= to; i--) {
                presenter.markPoint(x, i);
            }
        }

    };

    presenter.clear = presenter.reset;

    presenter.clearMark = presenter.resetMark;

    presenter.markPoint = function(x, y) {
        x = parseInt(x, 10);
        y = parseInt(y, 10);
        presenter.actualCursorPosition = [x,y];
        if (ModelValidationUtils.validateIntegerInRange(x, presenter.configuration.columns, 1).isValid != false) {
            if (ModelValidationUtils.validateIntegerInRange(y, presenter.configuration.rows, 1).isValid != false) {
                presenter.coloredGrid[y - 1][x - 1] = presenter.configuration.color;
            }
        }
        presenter.colorSquare(x, y);
    };

    presenter.mark = function mark (x, y) {
        presenter.markPoint(x, y);
        presenter.sendMarkEvent("mark", x, y);
    };

    presenter.sendMarkEvent = function (name, x, y) {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': name,
            'value': x +"-"+ y,
            'score': ''
        };
        sendValueChangedEvent(eventData);
    };

    presenter.sendDrawFromEvent = function (name, x, y, numberOfSteps) {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': name,
            'value': x +"-"+ y +"-"+ numberOfSteps,
            'score': ''
        };
        sendValueChangedEvent(eventData);
    };

    presenter.sendDrawEvent = function (name, x) {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': name,
            'value': x,
            'score': ''
        };
        sendValueChangedEvent(eventData);
    };

    presenter.drawLeft = function (x, y, numberOfSteps) {
        x = parseInt(x, 10);
        y = parseInt(y, 10);
        numberOfSteps = parseInt(numberOfSteps, 10);
        if (arguments.length == 1) {
            if (x &lt;= 0) return;
            presenter.drawHorizontalLine( presenter.actualCursorPosition[0] - 1, presenter.actualCursorPosition[0] - x, presenter.actualCursorPosition[1]);
            presenter.sendDrawEvent("drawLeft", x);
        } else {
            if (numberOfSteps &lt;= 0) return;
            presenter.drawHorizontalLine(x, x - numberOfSteps + 1 , y);
            presenter.sendDrawFromEvent("drawLeftFrom", x, y, numberOfSteps);
        }
    };

    presenter.drawLeftFrom = presenter.drawLeft;

    presenter.drawRight = function (x, y, numberOfSteps) {
        x = parseInt(x, 10);
        y = parseInt(y, 10);
        numberOfSteps = parseInt(numberOfSteps, 10);
        if (arguments.length == 1) {
            if (x &lt;= 0) return;
            presenter.drawHorizontalLine(presenter.actualCursorPosition[0] + 1, presenter.actualCursorPosition[0] + x, presenter.actualCursorPosition[1]);
            presenter.sendDrawEvent("drawRight", x);
        } else {
            if (numberOfSteps &lt;= 0) return;
            presenter.drawHorizontalLine(x, x + numberOfSteps - 1, y);
            presenter.sendDrawFromEvent("drawRightFrom", x, y, numberOfSteps);
        }
    };

    presenter.drawRightFrom = presenter.drawRight;

    presenter.drawUp = function (x, y, numberOfSteps) {
        x = parseInt(x, 10);
        y = parseInt(y, 10);
        numberOfSteps = parseInt(numberOfSteps);
        if (arguments.length == 1) {
            if (x &lt;= 0) return;
            presenter.drawVerticalLine(presenter.actualCursorPosition[1] + 1, presenter.actualCursorPosition[1] + x, presenter.actualCursorPosition[0]);
            presenter.sendDrawEvent("drawUp", x);
        } else {
            if (numberOfSteps &lt;= 0) return;
            presenter.drawVerticalLine(y, y + numberOfSteps - 1, x);
            presenter.sendDrawFromEvent("drawUpFrom", x, y, numberOfSteps);
        }
    };

    presenter.drawUpFrom = presenter.drawUp;

    presenter.drawDown = function (x, y, numberOfSteps) {
        x = parseInt(x, 10);
        y = parseInt(y, 10);
        numberOfSteps = parseInt(numberOfSteps, 10);
        if (arguments.length == 1) {
            if (x &lt;= 0) return;
            presenter.drawVerticalLine(presenter.actualCursorPosition[1] - 1, presenter.actualCursorPosition[1]  - x, presenter.actualCursorPosition[0]);
            presenter.sendDrawEvent("drawDown", x);
        } else {
            if (numberOfSteps &lt;= 0) return;
            presenter.drawVerticalLine(y, y - numberOfSteps + 1, x);
            presenter.sendDrawFromEvent("drawDownFrom", x, y, numberOfSteps);
        }
    };

    presenter.drawDownFrom = presenter.drawDown;

    presenter.setColor = function (color) {
        if (color.trim() === '') {
            return;
        }
        presenter.configuration.color = color;
    };

    presenter.generateCommand = function Grid_Scene_generate_command (code, name, args) {
        return eval ("(function() { return function(" + args + "){" + code + "}}())");
    };

    function isExcluded(name, excludedCommands) {
        if ((excludedCommands[name] != null) &amp;&amp; (excludedCommands[name])) {
            return true;
        }
        return false;
    }

    presenter.getSceneCommands = function () {
        var commandsLabels = $.extend({}, presenter.LABELS, presenter.configuration.labels);

        var commands = $.extend(true, {}, presenter.originalCommands);
        var excludedCommands = presenter.configuration.excludedCommands;

        for (var key in excludedCommands) {
            if (excludedCommands.hasOwnProperty(key)) {
                if (commands.hasOwnProperty(key)) {
                    commands[key] = delayDecorator(presenter.generateCommand("", "empty", ""));
                }
            }
        }

        var result = {};
        for (var key in commands) {
            var label = commandsLabels[key];
            result[label] = commands[key];
        }

        return result;
    };

    presenter.getCustomCommandsToEval = function () {
        var customCommands = presenter.configuration.customCommands;
        var excludedCommands = presenter.configuration.excludedCommands;
        var customCommandsString = "";
        for (var index = 0; index &lt; customCommands.length; index++) {
            var customCommand = customCommands[index];
            if (!isExcluded(customCommand['name'], excludedCommands)) {
                customCommandsString += "function " + customCommand['name'] + "(" + customCommand['arguments'] + "){" + customCommand['code'] + "};";
            }
            else {
                customCommandsString += "function " + customCommand['name'] + "(){};";
            }
        }
        return customCommandsString;
    };

    presenter.executeCode = function (code) {
        if (presenter.configuration.isShowingAnswers || presenter.configuration.isErrorMode)  {
            return;
        }

        with (presenter.getSceneCommands()) {
            var customCommands = presenter.getCustomCommandsToEval();
            try {
                eval(customCommands);
                eval(code);
            } catch (e) { }

        }
        sendRunEvent();

        if (presenter.isAllOK()) {
            sendAllOKEvent();
        }
    };

    presenter.getState = function Grid_Scene_get_state () {
        if(presenter.configuration.isShowingAnswers) {
            presenter.hideAnswers();
        }

        return JSON.stringify( {
            grid: presenter.coloredGrid,
            visibility: presenter.configuration.isVisible,
            color: presenter.configuration.color
        });
    };

    presenter.setState = function Grid_Scene_set_state (state) {
        if (state != null) {
            var state = JSON.parse(state);
            presenter.setColoredGridArray(state.grid);
            presenter.setVisibility(state.visibility);
            presenter.setColor(state.color);
        }
    };

    presenter.getMaxScore = function Grid_Scene_get_max_score () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        return 1;
    };

    presenter.getScore = function Grid_Scene_get_score () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        if (presenter.configuration.isShowingAnswers) {
            return 0;
        }

        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        var answer = presenter.configuration.answer;
        var actualState = presenter.coloredGrid;
        for (var i = 0; i &lt; rows; i++) {
            for (var j = 0; j &lt; columns; j++) {
                if (actualState[i][j] != answer[i][j]) {
                    return 0;
                }
            }
        }
        return 1;
    };

    presenter.getErrorCount = function () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        var errors = 0;
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        var answer = presenter.configuration.answer;
        var actualState = presenter.coloredGrid;
        for (var i = 0; i &lt; rows; i++) {
            for (var j = 0; j &lt; columns; j++) {
                if (actualState[i][j] == "Empty")
                    continue;
                if (actualState[i][j] != answer[i][j]) {
                    errors ++;
                }
            }
        }
        return errors;
    };

    presenter.setWorkMode = function () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        for (var i = 0; i &lt; columns; i++){
            for (var j = 0; j &lt; rows; j++){
                var coordinates = (i + 1) + "-" +(1 + j);
                var element = presenter.$view.find('.grid-scene-cell-element[coordinates="' + coordinates + '"]');
                element.removeClass('grid-scene-wrong').removeClass('grid-scene-correct');
            }
        }
        presenter.configuration.isErrorMode = false;
        presenter.setColoredGridArray(presenter.coloredGrid);
        presenter.actualCursorPosition = presenter.currentCursorPosition;
    };

    presenter.setShowErrorsMode = function () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        if (presenter.configuration.isShowingAnswers) {
            presenter.hideAnswers();
        }
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        var answer = presenter.configuration.answer;
        var actualState = presenter.coloredGrid;

        for (var i = 0; i &lt; columns; i++){
            for (var j = 0; j &lt; rows; j++){
                var coordinates = (i + 1) + "-" +(j + 1);
                var element = presenter.$view.find('.grid-scene-cell-element[coordinates="' + coordinates + '"]');
                element.css('background-color', '');
                if (actualState[j][i] != "Empty") {
                    if (answer[j][i] == actualState[j][i]) {
                        element.addClass('grid-scene-correct');
                    } else {
                        element.addClass('grid-scene-wrong');
                    }
                }
            }
        }

        presenter.currentCursorPosition = presenter.actualCursorPosition;

        presenter.configuration.isErrorMode = true;
    };

    presenter.showAnswers = function AddonIFrame_Communication_show_answers () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }

        presenter.beforeSACursorPosition = presenter.actualCursorPosition;

        if(presenter.configuration.isErrorMode) {
            presenter.setWorkMode();
        }
        presenter.lastState = $.extend(true,[], presenter.coloredGrid);
        presenter.reset();
        presenter.configuration.isShowingAnswers = true;
        presenter.setColoredGridArray(presenter.configuration.answer);
    };

    presenter.hideAnswers = function AddonIFrame_Communication_hide_answers () {
        if (!presenter.configuration.isAnswer) {
            return 0;
        }
        presenter.configuration.isShowingAnswers = false;
        presenter.reset();
        presenter.coloredGrid = presenter.lastState;
        presenter.lastState = null;
        presenter.setColoredGridArray(presenter.coloredGrid);
        presenter.actualCursorPosition = presenter.beforeSACursorPosition;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.isAllOK = function () {
        if (!presenter.configuration.isSavingAnswer) {
            if (!presenter.configuration.isPreview) {
                var rows = presenter.configuration.rows;
                var columns = presenter.configuration.columns;
                var answer = presenter.configuration.answer;
                var actualState = presenter.coloredGrid;
                for (var i = 0; i &lt; rows; i++) {
                    for (var j = 0; j &lt; columns; j++) {
                        if (actualState[i][j] != answer[i][j]) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    };

    presenter.getBlocklyData = function () {
        var excludedBlocks = {};
        for (var key in presenter.DEFAULT_COMMANDS_TO_BLOCKS) {
            if (presenter.DEFAULT_COMMANDS_TO_BLOCKS.hasOwnProperty(key)) {
                if (!presenter.configuration.excludedCommands.hasOwnProperty(key)) {
                    excludedBlocks[presenter.DEFAULT_COMMANDS_TO_BLOCKS[key]] = true;
                }
            }
        }
        return {
            "type": "GridScene",
            "labels": $.extend({}, presenter.LABELS, presenter.configuration.labels),
            "availableBlocks" : excludedBlocks

        }
    };

    function sendRunEvent () {
        sendValueChangedEvent({
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': 1,
            'score': ''
        });
    }

    function sendAllOKEvent() {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };
        sendValueChangedEvent(eventData);
    }

    function sendValueChangedEvent(eventData) {
        if (presenter.eventBus != null) {
            presenter.eventBus.sendEvent('ValueChanged', eventData);
        }
    }


    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Hangman" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Phrases" nameLabel="Hangman_property_phrases" type="list">
			<property isLocalized="true" name="Letters" nameLabel="Hangman_property_letters" type="string"/>
			<property isLocalized="true" name="Phrase" nameLabel="Hangman_property_phrase" type="string"/>
		</property>
		<property name="Possible mistakes" nameLabel="Hangman_property_possible_mistakes" type="string"/>
        <property displayName="Is Not Activity" name="isNotActivity" nameLabel="Hangman_property_is_not_activity" type="boolean"/>
        <property displayName="Keyboard Letters Order" name="Keyboard Letters Order" nameLabel="Hangman_property_keyboard_letters_order" type="string"/>
	</model>
<css>.hangman-wrapper {
    height: 100%;
    text-align: center;
}

.hangman-letter {
    float: left;
    margin: 3px;
    padding: 3px;
    border: 1px solid black;
    border-radius: 3px;
    width: 20px;
    height: 20px;
}

.hangman-letters,
.hangman-phrase {
    border: 2px solid black;
    border-radius: 5px;
    width: 97%;
    height: 48%;
}

.hangman-letters {
    margin-bottom: 5px;
}

.hangman-phrase-word {
    margin-left: 5px;
    margin-right: 10px;
    float: left;
}

.hangman-phrase-word .hangman-letter {
    border: none;
    border-radius: 0;
}

.hangman-phrase-word .hangman-letter.empty {
    border-bottom: 3px solid black;
}

.hangman-letters .hangman-letter.selected {
    background-color: #dcdcdc;
}

.hangman-letters .hangman-letter.correct {
    background-color: #adff2f;
}

.hangman-letters .hangman-letter.incorrect {
    background-color: red;
}

.hangman-letters .hangman-letter.show-answers {
    background-color: blue;
}
</css><view>&lt;div class="hangman-wrapper"&gt;
    &lt;div class="hangman-letters"&gt;&lt;/div&gt;
    &lt;div class="hangman-phrase"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="hangman-wrapper"&gt;
    &lt;div class="hangman-letters"&gt;&lt;/div&gt;
    &lt;div class="hangman-phrase"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonHangman_create() {
    var presenter = function () {};
    var playerController = null;
    var eventBus;

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
        eventBus = playerController.getEventBus();

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    presenter.drawElements = function (phraseNumber) {
        var lettersInOrder = presenter.getLettersInOrder(presenter.configuration.phrases[phraseNumber].letters);
        presenter.configuration.lettersInCustomOrder = lettersInOrder;
        presenter.drawLetters(lettersInOrder);
        presenter.drawPhrase(presenter.$view.find('.hangman-phrase'), presenter.configuration.phrases[phraseNumber].phrase);
    };

    presenter.getLettersInOrder = function (letters) {
        if(presenter.configuration.isCustomKeyboardLettersOrderSet) {
            return presenter.changeLettersOrder(letters);
        }

        return letters;
    };

    presenter.getLettersFromKeyboardOrder = function (letters) {
        var lettersFromKeyboardOrder = [];

        presenter.configuration.keyboardLettersOrder.map(function (element) {
            if (letters.indexOf(element) != -1) {
                this.push(element);
            }
        }, lettersFromKeyboardOrder);

        return lettersFromKeyboardOrder
    };

    presenter.getRestOfLetters = function (orderedLetters, letters) {
        return orderedLetters.concat(letters.filter(function (element) {
            return this.indexOf(element) == -1;
        }, orderedLetters));
    };

    presenter.changeLettersOrder = function (letters) {
        var orderedLetters = presenter.getLettersFromKeyboardOrder(letters);
        return presenter.getRestOfLetters(orderedLetters, letters);
    };

    presenter.drawElementsAndAttachMouseHandlers = function (phraseNumber, isPreview) {
        presenter.drawElements(phraseNumber);

        if (isPreview) {
            presenter.showCorrect();
        } else {
            presenter.handleMouseActions();
        }
    };

    presenter.assignVariablesToPresenter = function (view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.currentPhrase = 0;
        presenter.isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.isVisibleByDefault = presenter.isVisible;
        presenter.$phraseContainer = $(view).find('.hangman-phrase');
        presenter.$lettersContainer = $(view).find('.hangman-letters');
        presenter.isErrorCheckingMode = false;
        presenter.isActivity = !(ModelValidationUtils.validateBoolean(model['isNotActivity']));
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeKeyboardLettersOrder(model);
    };

    presenter.upgradeKeyboardLettersOrder = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model["Keyboard Letters Order"] == undefined) {
            upgradedModel["Keyboard Letters Order"] = "";
        }

        return upgradedModel;
    };

    presenter.deleteCommands = function () {
        delete presenter.setState;
        delete presenter.getState;
        delete presenter.getScore;
        delete presenter.getMaxScore;
        delete presenter.reset;
        delete presenter.nextPhrase;
        delete presenter.previousPhrase;
        delete presenter.isAllOk;
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.configuration = presenter.sanitizeModel(upgradedModel);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            presenter.deleteCommands();
            return;
        }

        presenter.assignVariablesToPresenter(view, model);
        presenter.drawElementsAndAttachMouseHandlers(0, isPreview);

        presenter.addMarkedLetter(isPreview);
    };

    presenter.ERROR_CODES = {
        'L_01': "Letters definition incorrect!",
        'W_01': "Words definition cannot be empty!",
        'W_02': "Words definition consist letters that are not specified!",
        'W_03': "You cannot type more than one exclamation mark next to each other!",
        'W_04': "Words definition cannot contain only exclemation marks!",
        'P_01': "At least one phrase must be specified!",
        'T_01': "Number possible mistakes incorrect!",
        'KLO_01': "Letters in property Keyboard Letters Order incorrect.",
        'KLO_02': "Letters cant duplicate in Keyboard Letters Order property."
    };

    presenter.DEFAULT_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
        'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.isArrayWithLettersValid = function (arrayWithLetters) {
        for (var i = 0, length = arrayWithLetters.length; i &lt; length; i++) {
            var letter = arrayWithLetters[i].toUpperCase().trim();
            if (letter) {
                if (letter.length &gt; 1) {
                    return false;
                }
            }
        }

        return true;
    };

    /**
     Converts string representation of list to array. Additionally trims elements, upper cases them and
     removes duplicated entries.
     @method convertStringArray
     @param {String} letters string containing comma separated, single character elements (whitespaces allowed)
     @return {Array} converted and sorted (ascending) array
     */
    presenter.convertStringArray = function (letters) {
        if (ModelValidationUtils.isStringEmpty(letters)) return presenter.DEFAULT_LETTERS;

        var list = [];
        var lettersArray = letters.split(',');
        for (var i = 0, length = lettersArray.length; i &lt; length; i++) {
            var letter = lettersArray[i].toUpperCase().trim();
            if (letter &amp;&amp; list.indexOf(letter) === -1) {
                list.push(letter);
            }
        }

        return list.sort();
    };

    presenter.wordsMatchLetters = function(letters, words) {
        var wordsArray = words.split(' ');

        for (var i = 0; i &lt; wordsArray.length; i++) {
            var word = wordsArray[i];

            for (var j = 0; j &lt; word.length; j++) {
                if (letters.indexOf(word[j].toUpperCase()) === -1 &amp;&amp; word[j] != '!')  return false;
            }
        }

        return true;
    };

    presenter.isCorrectCountOfEcxlamationMarks = function(letters, words) {
        var wordsArray = words.split(' ');

        for (var i = 0; i &lt; wordsArray.length; i++) {
            var word = wordsArray[i];

            for (var j = 0; j &lt; word.length; j++) {
                if (word[j] == '!' &amp;&amp; word[j+1] == '!')  return false;
            }
        }

        return true;
    };

    presenter.isOnlyExclamationMark = function(letters, words) {
        var wordsArray = words.split(' ');

        for (var i = 0; i &lt; wordsArray.length; i++) {
            var word = wordsArray[i];
            if(word == '!') return false;
        }

        return true;
    };

    presenter.sanitizePhrase = function (phrase) {
        if (ModelValidationUtils.isStringEmpty(phrase.trim())) return returnErrorObject('W_01');

        var phraseArray = [];
        var splittedPhrase = phrase.trim().split(' ');

        for (var i = 0; i &lt; splittedPhrase.length; i++) {
            if (splittedPhrase[i]) {
                phraseArray.push(splittedPhrase[i].toUpperCase());
            }
        }

        return { isError: false, phrase: phraseArray };
    };

    presenter.sanitizePhrases = function(phrases) {
        if (ModelValidationUtils.isArrayEmpty(phrases)) {
            return returnErrorObject('P_01');
        }

        var sanitisedPhrases = [];

        for (var i = 0; i &lt; phrases.length; i++) {
            if (!presenter.isArrayWithLettersValid(phrases[i].Letters.split(","))) return returnErrorObject('L_01');

            if (ModelValidationUtils.isStringEmpty(phrases[i].Phrase)) return returnErrorObject('W_01');

            var letters = presenter.convertStringArray(phrases[i].Letters);
            if (!presenter.wordsMatchLetters(letters, phrases[i].Phrase)) return returnErrorObject('W_02');
            if (!presenter.isCorrectCountOfEcxlamationMarks(letters, phrases[i].Phrase)) return returnErrorObject('W_03');
            if (!presenter.isOnlyExclamationMark(letters, phrases[i].Phrase)) return returnErrorObject('W_04');

            var sanitisedPhrase = presenter.sanitizePhrase(phrases[i].Phrase);
            if (sanitisedPhrase.isError) return sanitisedPhrase;

            sanitisedPhrases.push({
                letters: letters,
                phrase: sanitisedPhrase.phrase,
                errorCount: 0,
                selectedLetters: []
            });
        }

        return {isError: false, phrases: sanitisedPhrases };
    };

    presenter.validateTrialsCount = function (model) {
        var validatedInt = ModelValidationUtils.validatePositiveInteger(model['Possible mistakes']);
        if (!validatedInt.isValid) {
            return { isValid: false, errorCode: "T_01", isError: true};
        }

        return validatedInt;
    };

    presenter.sanitizeModel = function (model) {
        var sanitisedPhrases = presenter.sanitizePhrases(model.Phrases);

        if (sanitisedPhrases.isError) return sanitisedPhrases;

        var validatedTrialsCount = presenter.validateTrialsCount(model);
        if (!validatedTrialsCount.isValid) return validatedTrialsCount;

        var validatedKeyboardLettersOrder = presenter.validateKeyboardLettersOrder(model);
        if (validatedKeyboardLettersOrder.isError) {
            return validatedKeyboardLettersOrder;
        }

        return {
            isError: false,
            phrases: sanitisedPhrases.phrases,
            trialsCount: validatedTrialsCount.value,
            addonID: model.ID,
            keyboardLettersOrder: validatedKeyboardLettersOrder.value,
            isCustomKeyboardLettersOrderSet: validatedKeyboardLettersOrder.isCustomKeyboardLettersOrderSet,
            lettersInCustomOrder: []
        };
    };

    function isNotDuplicated(value) {
        return (this.filter(function (currentValue) {
                return (value == currentValue);
            }).length == 1
        );
    }

    presenter.validateKeyboardLettersOrder = function (model) {
        var keyboardLettersOrder = model["Keyboard Letters Order"];
        if (ModelValidationUtils.isStringEmpty(keyboardLettersOrder.trim())) {
            return {
                isError: false,
                value: [],
                isCustomKeyboardLettersOrderSet: false
            };
        }

        var preparedData = keyboardLettersOrder.split(",").map(function (element) {
            return element.trim().toUpperCase();
        });

        if (!presenter.isArrayWithLettersValid(preparedData)) {
            return returnErrorObject("KLO_01");
        }

        if (!preparedData.every(isNotDuplicated, preparedData)) {
            return returnErrorObject("KLO_02");
        }

        return {
            isError: false,
            value: preparedData,
            isCustomKeyboardLettersOrderSet: true
        };
    };

    function generateLetter() {
        var $element = $(document.createElement('div'));
        $element.addClass('hangman-letter');
        $element.text(' ');

        return $element;
    }

    function generateLetterWithText(letter) {
        var $element = generateLetter();
        $element.text(letter);

        return $element;
    }

    presenter.drawLetters = function (letters) {
        for (var i = 0; i &lt; letters.length; i++) {
            presenter.$lettersContainer.append(generateLetterWithText(letters[i]));
        }
    };

    function generatePhraseWord(word) {
        var $element = $(document.createElement('div'));
        $element.addClass('hangman-phrase-word');

        for (var j = 0; j &lt; word.length; j++) {
            var $letter;
            if(word[j].indexOf('!') &gt; -1){
                var $elementLetter = $(document.createElement('div'));
                $elementLetter.addClass('hangman-letter');
                $elementLetter.text('!'+word[j+1]);
                $element.append($elementLetter);
            }else{
                $letter = generateLetter();
                $letter.addClass('empty');

                $element.append($letter);
            }
        }

        return $element;
    }

    presenter.drawPhrase = function ($container, phrase) {
        for (var i = 0; i &lt; phrase.length; i++) {
            $container.append(generatePhraseWord(phrase[i]));
        }
    };

    presenter.disableRemainingLetters = function () {
        presenter.$lettersContainer.find('.hangman-letter').each(function (_, element) {
            if (!$(element).hasClass('selected')) {
                $(element).unbind('click');
            }
        });
    };

    presenter.unbindAttachedHandlers = function ($element) {
        $element.unbind('click');
        if (presenter.$phraseContainer.find('.hangman-letter.empty').length === 0) {
            presenter.disableRemainingLetters();
        }
    };
    presenter.onLetterSelectedAction = function (letter, currentPhrase, sendEventAndCountError) {
        var findResult = presenter.findLetterInPhrase(letter, currentPhrase.phrase);
        var selectionEventData;

        if (findResult.length === 0) {
            selectionEventData = presenter.createLetterSelectedEventData(letter, false);

            if (sendEventAndCountError) {
                currentPhrase.errorCount++;
            }
            if (currentPhrase.errorCount &gt; presenter.configuration.trialsCount) {
                presenter.disableRemainingLetters();
            }
        } else {
            presenter.fillPhraseWithLetters(letter, findResult);
            selectionEventData = presenter.createLetterSelectedEventData(letter, true);
        }

        if (sendEventAndCountError) {
            presenter.sendEventData(selectionEventData);
            if (presenter.isAllOK() &amp;&amp; presenter.isActivity) {
                presenter.sendAllOKEvent();
            }
        }
        if (currentPhrase.errorCount === presenter.configuration.trialsCount &amp;&amp; presenter.isActivity &amp;&amp; !currentPhrase.EndOfTrialsWasSent) {
            presenter.sendEventData(presenter.createEndOfTrialsEventData());
            currentPhrase.EndOfTrialsWasSent = true;
        } else if (currentPhrase.errorCount &gt; presenter.configuration.trialsCount &amp;&amp; presenter.isActivity &amp;&amp; !currentPhrase.endOfGame) {
            presenter.sendEndOfGameEvent();
            currentPhrase.endOfGame = true;
        }

        presenter.unbindAttachedHandlers($(this));
    };

    presenter.sendEndOfGameEvent = function () {
        if (presenter.isShowAnswersActive) {
            return;
        }

        var eventData = presenter.createBaseEventData();

        eventData.value = 'EOG';
        eventData.score = '';

        presenter.sendEventData(eventData);
    };

    presenter.letterClickHandler = function (e) {
        if(presenter.isErrorCheckingMode){
            return;
        }

        e.stopPropagation();
        var sendEventAndCountError = !$(this).hasClass('selected');
        $(this).addClass('selected');
        var letter = $(this).text();

        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];
        presenter.addLetterSelectionToPhrase(currentPhrase, letter);
        presenter.onLetterSelectedAction(letter, currentPhrase, sendEventAndCountError);
    };

    presenter.handleMouseActions = function () {
        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];

        presenter.$lettersContainer.find('.hangman-letter').each(function (_, element) {
            var letter = $(element).text();

            if (!presenter.isLetterSelected(currentPhrase, letter)) {
                $(element).click(presenter.letterClickHandler);
            }
        });
    };

    presenter.findLetterInPhrase = function (letter, phrase) {
        var occurrence = [];

        for (var i = 0; i &lt; phrase.length; i++) {
            for (var j = 0; j &lt; phrase[i].length; j++) {
                if (phrase[i][j] === letter) {
                    occurrence.push({ word: i, index: j});
                }
            }
        }

        return occurrence;
    };

    presenter.fillPhraseWithLetters = function (letter, letterOccurrence) {
        var phrase = presenter.configuration.phrases[presenter.currentPhrase].phrase;

        for (var i = 0; i &lt; letterOccurrence.length; i++) {
            var index = presenter.calculateLetterElementIndex(phrase, letterOccurrence[i]);

            var $letterElement = presenter.$phraseContainer.find('.hangman-letter:eq(' + index + ')');
            $letterElement.removeClass('empty');
            $letterElement.text(letter);
        }
    };

    presenter.calculateLetterElementIndex = function (phrase, occurrence) {
        var index = 0;

        for (var i = 0; i &lt; occurrence.word; i++) {
            index += phrase[i].length;
        }

        return index + occurrence.index;
    };

    presenter.getIndexOfLetterInPhrase = function (phrase, letter) {
        var index = phrase.letters.indexOf(letter);

        if (presenter.configuration.isCustomKeyboardLettersOrderSet) {
            index = presenter.configuration.lettersInCustomOrder.indexOf(letter);
        }

        return index;
    };

    presenter.addLetterSelectionToPhrase = function (phrase, letter) {
        var index = presenter.getIndexOfLetterInPhrase(phrase, letter);

        phrase.selectedLetters.push(index);
    };

    presenter.isLetterSelected = function (phrase, letter) {
        var index = presenter.getIndexOfLetterInPhrase(phrase, letter);

        return phrase.selectedLetters.indexOf(index) !== -1;
    };

    presenter.showCorrect = function () {
        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];
        var neededLetters = presenter.getNeededLetters(currentPhrase.phrase);
        var $letter;
        for (var i = 0; i &lt; neededLetters.length; i++) {
            if(neededLetters[i].indexOf('!') &gt; -1){
                $letter = $('&lt;div&gt;'+neededLetters[i]+'&lt;/div&gt;');
                $letter.addClass('hangman-letter');
            }else{
                $letter = presenter.findLetterElement(neededLetters[i]);
            }

            if (!presenter.isLetterSelected(currentPhrase, neededLetters[i])) {
                $letter.addClass('selected');
                if (presenter.isErrorCheckingMode) {
                    if(!$letter.hasClass('hangman-tip')){
                        $letter.addClass('incorrect');
                    }
                }
                presenter.addLetterSelectionToPhrase(currentPhrase, $letter.text());
                presenter.onLetterSelectedAction(neededLetters[i], currentPhrase, false);
            } else if (presenter.isErrorCheckingMode) {
                if(!$letter.hasClass('hangman-tip')){
                    $letter.addClass('correct');
                }
            }
        }

        if (presenter.isErrorCheckingMode) {
            presenter.$lettersContainer.find('.hangman-letter.selected:not(.correct):not(.incorrect)').each(function (_, element) {
                if(!$(element).hasClass('hangman-tip')){
                    $(element).addClass('incorrect');
                }
            });
        }
    };

    presenter.showCorrectInSetShowErrorsMode = function () {
        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];
        var neededLetters = presenter.getNeededLetters(currentPhrase.phrase);
        var $letter;
        for (var i = 0; i &lt; neededLetters.length; i++) {
            if(neededLetters[i].indexOf('!') &gt; -1){
                $letter = $('&lt;div&gt;'+neededLetters[i]+'&lt;/div&gt;');
                $letter.addClass('hangman-letter');
            }else{
                $letter = presenter.findLetterElement(neededLetters[i]);
            }

            if (presenter.isLetterSelected(currentPhrase, neededLetters[i])) {
                if(!$letter.hasClass('hangman-tip')){
                    $letter.addClass('correct');
                }
            }
        }

        presenter.$lettersContainer.find('.hangman-letter.selected:not(.correct):not(.incorrect)').each(function (_, element) {
            if(!$(element).hasClass('hangman-tip')){
                $(element).addClass('incorrect');
            }
        });
    };

    presenter.findLetterElement = function (letter) {
        var $letters = presenter.$lettersContainer.find('.hangman-letter');

        for (var i = 0; i &lt; $letters.length; i++) {
            if ($($letters[i]).text() === letter)  return $($letters[i]);
        }
    };

    presenter.getNeededLetters = function (phrase) {
        var letters = [];
        for (var i = 0; i &lt; phrase.length; i++) {
            for (var j = 0; j &lt; phrase[i].length; j++) {
                if (letters.indexOf(phrase[i][j]) === -1) {
                    if(phrase[i][j] == '!'){
                        letters.push('!' + phrase[i][j+1]);
                    }else{
                        letters.push(phrase[i][j]);
                    }
                }
            }
        }

        return letters;
    };

    presenter.nextPhrase = function () {
        presenter.switchPhrase(presenter.currentPhrase + 2);
    };

    presenter.previousPhrase = function () {
        presenter.switchPhrase(presenter.currentPhrase);
    };

    presenter.switchPhraseCommand = function (params) {
        var phraseNumber = parseInt(params[0], 10);

        if (isNaN(phraseNumber) || phraseNumber &lt;= 0 || phraseNumber &gt; presenter.configuration.phrases.length) {
            return;
        }

        presenter.removeChildrenElements(presenter.$phraseContainer);
        presenter.removeChildrenElements(presenter.$lettersContainer);

        presenter.currentPhrase = phraseNumber - 1;
        presenter.drawElementsAndAttachMouseHandlers(phraseNumber - 1, false);
        presenter.applySelection();

        presenter.addMarkedLetter(false);
    };

    presenter.switchPhrase = function (phraseNumber) {
        presenter.switchPhraseCommand([phraseNumber]);
    };

    presenter.applySelection = function () {
        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];

        for (var i = 0; i &lt; currentPhrase.selectedLetters.length; i++) {
            var $letter = presenter.$lettersContainer.find('.hangman-letter:eq(' + currentPhrase.selectedLetters[i] + ')');
            $letter.addClass('selected');
            presenter.onLetterSelectedAction($letter.text(), currentPhrase, false);
        }
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorCheckingMode) {
            return;
        }

        var commands = {
            'showCorrect': presenter.showCorrect,
            'nextPhrase': presenter.nextPhrase,
            'previousPhrase': presenter.previousPhrase,
            'switchPhrase': presenter.switchPhraseCommand,
            'show': presenter.show,
            'hide': presenter.hide,
            'isAllOK': presenter.isAllOK
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.removeChildrenElements = function ($element) {
        $element.children().each(function (_, element) {
            $(element).remove();
        });
    };

    presenter.reset = function () {
        var phrases = presenter.configuration.phrases;

        for (var i = 0; i &lt; phrases.length; i++) {
            phrases[i].selectedLetters = [];
            phrases[i].errorCount = 0;
            phrases[i].endOfGame = false;
        }

        presenter.switchPhrase(1);
        presenter.isVisible = presenter.isVisibleByDefault;
        presenter.setVisibility(presenter.isVisible);
        presenter.addMarkedLetter(false);
        presenter.isErrorCheckingMode = false;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.getState = function () {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }

        return JSON.stringify({
            currentPhrase: presenter.currentPhrase,
            phrases: presenter.configuration.phrases
        });
    };

    presenter.upgradeStateForEndOfGame = function (parsedState) {
        if (parsedState.endOfGame == undefined) {
            parsedState.endOfGame = false;
        }

        return parsedState;
    };

    presenter.upgradeState = function (parsedState) {
        return presenter.upgradeStateForEndOfGame(parsedState);
    };

    presenter.addMarkedLetter = function (isPreview) {
        presenter.$view.find('.hangman-letter:contains(!)').next().css('display', 'none');
        var exclamationLetters = [];

        if(!isPreview){
            presenter.$view.find('.hangman-letter:contains(!)').each(function(_, element) {
                var text = $(this).text();
                var correctText = text.substring(1,2);
                exclamationLetters.push(correctText);
                $(element).text(correctText);
            });

            for(var i = 0; i&lt; presenter.configuration.phrases[presenter.currentPhrase].letters.length; i++){
                var merge = presenter.configuration.phrases[presenter.currentPhrase].phrase.join(),
                    letter = presenter.configuration.phrases[presenter.currentPhrase].letters[i],
                    count = merge.split(letter).length - 1,
                    exclMerge = exclamationLetters.join(),
                    exclCount = exclMerge.split(letter).length - 1;

                if(count == exclCount &amp;&amp; count &gt; 0 &amp;&amp; exclCount &gt; 0){
                    var $letter = presenter.$view.find('.hangman-letters').find('.hangman-letter:contains('+letter+')');
                    $letter.addClass('selected');
                    $letter.addClass('hangman-tip');
                    presenter.addLetterSelectionToPhrase(presenter.configuration.phrases[presenter.currentPhrase], $letter.text());
                }
            }
        }
    };

    presenter.setState = function (stringifiedState) {
        var state = JSON.parse(stringifiedState);
        var upgradedState = presenter.upgradeState(state);

        var phrases = presenter.configuration.phrases;

        for (var i = 0; i &lt; phrases.length; i++) {
            phrases[i].selectedLetters = upgradedState.phrases[i].selectedLetters;
            phrases[i].errorCount = upgradedState.phrases[i].errorCount;
            phrases[i].endOfGame = upgradedState.phrases[i].endOfGame;
        }

        presenter.switchPhrase(upgradedState.currentPhrase + 1);

        presenter.addMarkedLetter(false)
    };

    presenter.getMaxScore = function () {
        if(presenter.isActivity){
            return presenter.configuration.phrases.length;
        }else{
            return 0;
        }
    };

    presenter.isSelectionSufficient = function (neededLetters, selectedLetters) {
        if (selectedLetters.length &lt; neededLetters.length) return false;

        var sortedNeededLetters = neededLetters.sort();
        var sortedSelectedLetters = selectedLetters.sort();

        for (var i = 0; i &lt; sortedNeededLetters.length; i++) {
            if (sortedSelectedLetters.indexOf(sortedNeededLetters[i]) === -1) return false;
        }

        return true;
    };

    presenter.getLettersIndexesForScoring = function (neededLetters, phrase) {
        var neededLettersIndexes = [];

        if (presenter.configuration.isCustomKeyboardLettersOrderSet) {
            neededLetters.map(function (element) {
                var index = presenter.configuration.lettersInCustomOrder.indexOf(element);
                neededLettersIndexes.push(index);
            }, neededLettersIndexes);

            return neededLettersIndexes
        } else {

            for (var j = 0; j &lt; neededLetters.length; j++) {
                neededLettersIndexes.push(phrase.letters.indexOf(neededLetters[j]));
            }

            return neededLettersIndexes;
        }

    };

    presenter.getScoring = function (phrases) {
        var neededLetters = [], neededLettersIndexes = [];
        var score = 0, errors = 0;

        for (var i = 0; i &lt; phrases.length; i++) {
            neededLetters = presenter.getNeededLetters(phrases[i].phrase);

            for(var k = neededLetters.length; k--;){
                if (neededLetters[k].indexOf('!') &gt; -1) neededLetters.splice(k, 1);
            }

            neededLettersIndexes = presenter.getLettersIndexesForScoring(neededLetters, phrases[i]);

            if (presenter.isSelectionSufficient(neededLettersIndexes, phrases[i].selectedLetters)) {
                score++;
            } else {
                errors++;
            }
        }

        return { score: score, errors: errors };
    };

    presenter.getPhraseForScoring = function () {
        if (presenter.isErrorCheckingMode || presenter.isShowAnswersActive) {
            return JSON.parse(presenter.workModeState).phrases;
        } else {
            return presenter.configuration.phrases;
        }
    };

    presenter.isSomethingSelected = function () {
        return presenter.$view.find('.selected').length &gt; 0;
    };

    presenter.getScore = function () {
        if(presenter.isActivity){
            var phrases = presenter.getPhraseForScoring();
            return presenter.getScoring(phrases).score;
        }else{
            return 0;
        }
    };

    presenter.getErrorCount = function () {
        if(presenter.isActivity &amp;&amp; presenter.isSomethingSelected()){
            var phrases = presenter.getPhraseForScoring();
            return presenter.getScoring(phrases).errors;
        }else{
            return 0;
        }
    };

    presenter.setShowErrorsMode = function () {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }

        presenter.isErrorCheckingMode = true;
        if(presenter.isActivity){
            presenter.workModeState = presenter.getState();
            presenter.showCorrectInSetShowErrorsMode();
        }
    };

    presenter.setWorkMode = function () {
        presenter.isErrorCheckingMode = false;
        if(presenter.isActivity){
            presenter.setState(presenter.workModeState);
        }
    };

    presenter.createBaseEventData = function () {
        return {
            source: presenter.configuration.addonID,
            item: "" + (presenter.currentPhrase + 1),
            value: "",
            score: ""
        };
    };

    presenter.createEndOfTrialsEventData = function () {
        var eventData = presenter.createBaseEventData();

        eventData.value = 'EOT';
        eventData.score = '';

        return eventData;
    };

    presenter.createLetterSelectedEventData = function (letter, isCorrect) {
        var eventData = presenter.createBaseEventData();

        eventData.value = "" + letter;
        eventData.score = isCorrect ? "1" : "0";

        return eventData;
    };

    presenter.sendEventData = function (eventData) {
        if (playerController !== null) {
            playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore();
    };

    presenter.createAllOKEventData = function (){
        return{
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        }
    };

    presenter.sendAllOKEvent = function (){
        eventBus.sendEvent('ValueChanged', presenter.createAllOKEventData());
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showCorrectSA = function () {
        var currentPhrase = presenter.configuration.phrases[presenter.currentPhrase];
        var neededLetters = presenter.getNeededLetters(currentPhrase.phrase);
        var $letter;
        for (var i = 0; i &lt; neededLetters.length; i++) {
            if(neededLetters[i].indexOf('!') &gt; -1){
                $letter = $('&lt;div&gt;'+neededLetters[i]+'&lt;/div&gt;');
                $letter.addClass('hangman-letter');
            }else{
                $letter = presenter.findLetterElement(neededLetters[i]);
            }
            $letter.addClass('show-answers');
            presenter.addLetterSelectionToPhrase(currentPhrase, $letter.text());
            presenter.onLetterSelectedAction(neededLetters[i], currentPhrase, false);
        }
    };

    presenter.showAnswers = function () {
        if(!presenter.isActivity){
            return;
        }

        presenter.isShowAnswersActive = true;
        if(presenter.isErrorCheckingMode){
            presenter.setWorkMode();
        }

        presenter.workModeState = JSON.stringify({
            currentPhrase: presenter.currentPhrase,
            phrases: presenter.configuration.phrases
        });
        presenter.$view.find('.hangman-letter').each(function (){
            if($(this).hasClass('selected')){
                $(this).removeClass('selected');
            }
        });

        presenter.showCorrectSA();
    };

    presenter.hideAnswers = function () {
        if(!presenter.isActivity || !presenter.isShowAnswersActive) {
            return;
        }
        presenter.setState(presenter.workModeState);
        presenter.isShowAnswersActive = false;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Heading" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Heading" nameLabel="Heading_property_heading" type="{h1, h2, h3, h4, h5, h6}"/>
        <property isDefault="true" isLocalized="true" name="Content" nameLabel="Heading_property_content" type="html"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Heading_property_lang_attribute" type="string"/>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property name="isSection" nameLabel="printable_is_section" type="boolean"/>
        <property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
    </model>
<css/><view/><preview/><presenter>function AddonHeading_create () {

    function getErrorObject (ec) { return { isValid: false, errorCode: ec }; }

    var isVisibleByDefault = true;
    var isWCAGOn = false;
    var playerController = null;
    var printableController = null;
    var textParser = null;

    var presenter = function () {};

    presenter.ERROR_CODES = {
        C01: 'Property content cannot be empty.'
    };

    presenter.HEADINGS = {
        'h1': 'H1',
        'h2': 'H2',
        'h3': 'H3',
        'h4': 'H4',
        'h5': 'H5',
        'h6': 'H6',
        DEFAULT: 'h1'
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        const headingString = '&lt;[tag]&gt;&lt;/[tag]]&gt;'.replace('[tag]', presenter.configuration.heading);
        const $heading = $(headingString);

        const parsedContent = textParser.parseAltTexts(presenter.configuration.content);
        $heading.html(parsedContent);

        if (presenter.configuration.isTabindexEnabled) {
            $heading.attr("tabindex", "0");
        }

        presenter.$view.append($heading);
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeFrom_01(model);
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = "";
        }

        return upgradedModel;
    };

    presenter.validateModel = function (model) {
        if (ModelValidationUtils.isStringEmpty(model.Content)) {
            return getErrorObject('C01');
        }

        return {
            heading: ModelValidationUtils.validateOption(presenter.HEADINGS, model['Heading']).toLowerCase(),
            content: model.Content,

            ID: model.ID,
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']),
            langTag: model['langAttribute']
        };
    };

    presenter.executeCommand = function (name, params) {
        if (!presenter.configuration.isValid) {
            return;
        }

        isVisibleByDefault = presenter.configuration.isVisible;

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.reset = function () {
        presenter.setVisibility(isVisibleByDefault);
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        if (ModelValidationUtils.isStringEmpty(state)) return;

        var parsed = JSON.parse(state);
        var isVisible = parsed.isVisible;
        presenter.setVisibility(isVisible);
    };

    presenter.setPlayerController = function (controller) {
        playerController = controller;

        textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        textParser = new TextParserProxy(getTextParser());
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.keyboardController = function(keyCode, isShift, event) {
        event.preventDefault();
        if (keyCode == window.KeyboardControllerKeys.ENTER || keyCode == window.KeyboardControllerKeys.SPACE) {
            presenter.readContent();
        }
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.readContent = function() {
        var ttr = window.TTSUtils.getTextVoiceArrayFromElement(presenter.$view,presenter.configuration.langTag);
        speak(ttr);
    };

    presenter.isEnterable = function(){ return false;};

    presenter.setPrintableController = function (controller) {
        printableController = controller;
        textParser = new TextParserProxy(printableController.getTextParser());
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var model = presenter.upgradeModel(model);
        var configuration = presenter.validateModel(model);

        var $root = $('&lt;div&gt;&lt;/div&gt;');
        $root.attr('id',configuration.ID);
        $root.addClass('printable_addon_Heading');
        $root.css("max-width", model["Width"]+"px");
        $root.css("min-height", model["Height"]+"px");

        var $heading = $('&lt;[tag]&gt;&lt;/[tag]]&gt;'.replace('[tag]', configuration.heading));
        var parsedContent = configuration.content;
        parsedContent = textParser.parseAltTexts(parsedContent);
        $heading.html(parsedContent);
        $root.append($heading);

        return $root[0].outerHTML;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Hierarchical_Lesson_Report" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Title label" isLocalized="true" name="titleLabel" nameLabel="Hierarchical_Lesson_Report_property_title_label" type="string"/>
        <property displayName="Show results" name="results" nameLabel="Hierarchical_Lesson_Report_property_results" type="boolean"/>
        <property displayName="Results label" isLocalized="true" name="resultsLabel" nameLabel="Hierarchical_Lesson_Report_property_results_label" type="string"/>
        <property displayName="Show checks" name="checks" nameLabel="Hierarchical_Lesson_Report_property_checks" type="boolean"/>
        <property displayName="Checks label" isLocalized="true" name="checksLabel" nameLabel="Hierarchical_Lesson_Report_property_checks_label" type="string"/>
        <property displayName="Show errors" name="errors" nameLabel="Hierarchical_Lesson_Report_property_errors" type="boolean"/>
        <property displayName="Errors label" isLocalized="true" name="errorsLabel" nameLabel="Hierarchical_Lesson_Report_property_errors_label" type="string"/>
        <property displayName="Show mistakes" name="mistakes" nameLabel="Hierarchical_Lesson_Report_property_mistakes" type="boolean"/>
        <property displayName="Mistakes label" isLocalized="true" name="mistakesLabel" nameLabel="Hierarchical_Lesson_Report_property_mistakes_label" type="string"/>
        <property displayName="Show total" name="total" nameLabel="Hierarchical_Lesson_Report_property_total" type="boolean"/>
        <property displayName="Total label" isLocalized="true" name="totalLabel" nameLabel="Hierarchical_Lesson_Report_property_total_label" type="string"/>
        <property displayName="Depth of expand" name="expandDepth" nameLabel="Hierarchical_Lesson_Report_property_expand_depth" type="string"/>
        <property displayName="Row classes" name="classes" nameLabel="Hierarchical_Lesson_Report_property_classes" type="text"/>
        <property displayName="Show page scores" name="showpagescore" nameLabel="Hierarchical_Lesson_Report_property_show_page_scores" type="boolean"/>
        <property displayName="Page scores label" isLocalized="true" name="pageScoresLabel" nameLabel="Hierarchical_Lesson_Report_property_page_scores_label" type="string"/>
        <property displayName="Show max score award" name="showmaxscorefield" nameLabel="Hierarchical_Lesson_Report_property_show_max_score_fields" type="boolean"/>
        <property displayName="Max score award label" isLocalized="true" name="maxScoreAwardLabel" nameLabel="Hierarchical_Lesson_Report_property_max_score_award_label" type="string"/>
        <property displayName="Unvisited page scores label" isLocalized="true" name="unvisitedPageScoresLabel" nameLabel="Hierarchical_Lesson_Report_property_unvisited_page_scores_label" type="string"/>
        <property displayName="Disable score on pages" name="scoredisabled" nameLabel="Hierarchical_Lesson_Report_property_score_disabled" type="string"/>
        <property displayName="Enable pages" name="enablePages" nameLabel="Hierarchical_Lesson_Report_property_enable_pages" type="string"/>

        <property displayName="Alternative page titles" name="alternativePageTitles" nameLabel="Hierarchical_Lesson_Report_property_alternative_page_titles" type="list">
			<property displayName="Index" isLocalized="true" name="alternativePageNumber" nameLabel="Hierarchical_Lesson_Report_property_alternative_page_number" type="string"/>
            <property displayName="Is chapter" name="alternativePageIsChapter" nameLabel="Hierarchical_Lesson_Report_property_show_is_alternative_page_chapter" type="boolean"/>
			<property displayName="Page name" isLocalized="true" name="alternativePageName" nameLabel="Hierarchical_Lesson_Report_property_alternative_page_name" type="html"/>
		</property>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Hierarchical_Lesson_Report_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Hierarchical_Lesson_Report_property_speech_texts" type="staticlist">

            <property name="Expanded" nameLabel="Hierarchical_Lesson_Report_property_expanded_speech_text" type="staticrow">
                <property name="Expanded" nameLabel="Hierarchical_Lesson_Report_property_expanded_speech_text" type="string"/>
            </property>

            <property name="Collapsed" nameLabel="Hierarchical_Lesson_Report_property_collapsed_speech_text" type="staticrow">
                <property name="Collapsed" nameLabel="Hierarchical_Lesson_Report_property_collapsed_speech_text" type="string"/>
            </property>

            <property name="Results" nameLabel="Hierarchical_Lesson_Report_property_results_speech_text" type="staticrow">
                <property name="Results" nameLabel="Hierarchical_Lesson_Report_property_results_speech_text" type="string"/>
            </property>

            <property name="Checks" nameLabel="Hierarchical_Lesson_Report_property_checks_speech_text" type="staticrow">
                <property name="Checks" nameLabel="Hierarchical_Lesson_Report_property_checks_speech_text" type="string"/>
            </property>

            <property name="Mistakes" nameLabel="Hierarchical_Lesson_Report_property_mistakes_speech_text" type="staticrow">
                <property name="Mistakes" nameLabel="Hierarchical_Lesson_Report_property_mistakes_speech_text" type="string"/>
            </property>

            <property name="Errors" nameLabel="Hierarchical_Lesson_Report_property_errors_speech_text" type="staticrow">
                <property name="Errors" nameLabel="Hierarchical_Lesson_Report_property_errors_speech_text" type="string"/>
            </property>

            <property name="Score" nameLabel="Hierarchical_Lesson_Report_property_score_speech_text" type="staticrow">
                <property name="Score" nameLabel="Hierarchical_Lesson_Report_property_score_speech_text" type="string"/>
            </property>

            <property name="OutOf" nameLabel="Hierarchical_Lesson_Report_property_out_of_speech_text" type="staticrow">
                <property name="OutOf" nameLabel="Hierarchical_Lesson_Report_property_out_of_speech_text" type="string"/>
            </property>

            <property name="Total" nameLabel="Hierarchical_Lesson_Report_property_total_speech_text" type="staticrow">
                <property name="Total" nameLabel="Hierarchical_Lesson_Report_property_total_speech_text" type="string"/>
            </property>
        </property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property displayName="Use weighted arithmetic mean" name="isWeightedArithmeticMean" nameLabel="Hierarchical_Lesson_Report_property_is_weighted_arithmetic_mean" type="boolean"/>
        <property displayName="Exclude unvisited pages in the total" name="excludeUnvisitedPages" nameLabel="Hierarchical_Lesson_Report_property_exclude_unvisited_pages" type="boolean"/>
    </model>
<css>.hier_report {
    padding: 15px;
    border-radius: 5px;
    border: 1px solid gray;
    background-color: white;
    color: gray;
    overflow: auto;
}

.hier_report td{
    padding: 10px;
}

.hier_report-header{
    font-weight: bold;
    color: black !important;
    background-color: #bde0ff;
}
.hier_report-footer{
    font-weight: bold;
    color: black !important;
    background-color: #bde0ff;
}

.hier_report-chapter{
    background-color: Lavender;
}

.hier_report-odd{
    background-color: AliceBlue;
}

.hier_report-even{
    background-color: #f0f1ff;
}

.hier_report-progress {
    text-align:center;
    color:#0A8DBD;
    display: inline-block;
    width: 190px;
}

.hier_report-checks {
    width: 10%;
    color: green;
    text-align: center;
}

.hier_report-mistakes {
    width: 10%;
    color: #990000;
    text-align: center;
}

.hier_report-errors {
    width: 10%;
    color: red;
    text-align: center;
}

.hier_report-page-score {
    width: 10%;
    color: blue;
    text-align: center;
}

.hier_report-page-non-max-score,
.hier_report-page-max-score {
    width: 10%;
    text-align: center;
}

.ui-progressbar {
    padding:2px;
    width:	130px  !important;
    height: 15px    !important;
    border-radius: 5px;
    border:2px solid #02789F;
    background-color: #3CC6CD;
    border-radius: 5px;
    float: left;
}

.hier_drag-source{
    border: 1px solid orange;
}

.hier_report .treegrid-expander {
	float: left;
}

.hier_report .treegrid-indent {
	float: left;
}

.hier_report .text-wrapper {
	overflow: hidden;
}

.printable_hier_report {
    background-color: white;
    color: gray;
    overflow: auto;
}

.printable_hier_report-table {
    border-collapse: collapse;
    width: 100%;
}

.printable_hier_report td{
    padding: 10px;
}

.printable_hier_report-header{
    font-weight: bold;
    color: black !important;
    border-bottom: 1px solid #ccc;
}

.printable_hier_report-footer{
    font-weight: bold;
    color: black !important;
    border-top: 1px solid #ccc;
}

.printable_hier_report-header-progress {
    width: 190px;
    text-align: center;
}

.printable_hier_report-progress {
    width: 10%;
    text-align: center;
}

.printable_hier_report-checks {
    width: 10%;
    text-align: center;
}

.printable_hier_report-mistakes {
    width: 10%;
    text-align: center;
}

.printable_hier_report-errors {
    width: 10%;
    text-align: center;
}

.printable_hier_report-page-score {
    width: 10%;
    text-align: center;
}

.printable_hier_report-page-non-max-score,
.printable_hier_report-page-max-score {
    width: 10%;
    text-align: center;
}

.printable_hier_report-indent {
    float: left;
	width: 16px;
    height: 16px;
    display: inline-block;
    position: relative;
}

.printable_hier_report .text-wrapper {
	overflow: hidden;
}
</css><view> &lt;div class="hier_report"&gt;
     &lt;table  style="width: 100%"&gt;
     &lt;/table&gt;
 &lt;/div&gt;

</view><preview>&lt;div class="hier_report"&gt;
    &lt;table  style="width: 100%"&gt;
    &lt;/table&gt;
&lt;/div&gt;
</preview><presenter>function AddonHierarchical_Lesson_Report_create() {
    var presenter = function () {};
    var presentationController;
    var printableController;
    var isWCAGOn = false;

    // Indexes used during tree building
    var relativeIndex = 0;            // Current chapter index + current reportable page index
    var absoluteIndex = 0;            // Current chapter index + current page index
    var chapterIndex = 0;             // current chapter index
    var pageIndex = 0;                // current page index

    var currentRow = 1;
    var currentColumn = 0;

    presenter.printableState = null;
    presenter.printableStateMode = null;
    presenter.userVisitedPages = [];
    presenter.totalScore = 0; // only for ExcludedUnvisitedPagesInTotal
    presenter.printableTotalScore = 0; // only for ExcludedUnvisitedPagesInTotal

    var CSS_CLASSES = {
        SELECTED_CELL: "keyboard_navigation_active_element",
        TEXT_WRAPPER: "text-wrapper",
        HIER_REPORT: "hier_report",
        HIER_REPORT_HEADER: "hier_report-header",
        HIER_REPORT_FOOTER: "hier_report-footer",
        HIER_REPORT_CHAPTER: "hier_report-chapter",
        HIER_REPORT_SEPARATOR: "hier_report-separator",
        HIER_REPORT_PROGRESSBAR: "hier_report-progressbar",
        HIER_REPORT_ODD: "hier_report-odd",
        HIER_REPORT_EVEN: "hier_report-even",
        HIER_REPORT_PROGRESS: "hier_report-progress",
        HIER_REPORT_CHECKS: "hier_report-checks",
        HIER_REPORT_MISTAKES: "hier_report-mistakes",
        HIER_REPORT_ERRORS: "hier_report-errors",
        HIER_REPORT_PAGE_SCORE: "hier_report-page-score",
        HIER_REPORT_PAGE_MAX_SCORE: "hier_report-page-max-score",
        HIER_REPORT_PAGE_NON_MAX_SCORE: "hier_report-page-non-max-score",
        HIER_REPORT_SCORE_DISABLED_ROW: "hier_report-score-disabled-row",
        PRINTABLE: "printable_addon_Hierarchical_Lesson_Report",
        PRINTABLE_HIER_REPORT: "printable_hier_report",
        PRINTABLE_HIER_REPORT_TABLE: "printable_hier_report-table",
        PRINTABLE_HIER_REPORT_PARENT: "printable_hier_report-parent",
        PRINTABLE_HIER_REPORT_NODE: "printable_hier_report-node",
        PRINTABLE_HIER_REPORT_CHAPTER: "printable_hier_report-chapter",
        PRINTABLE_HIER_REPORT_SEPARATOR: "printable_hier_report-separator",
        PRINTABLE_HIER_REPORT_ODD: "printable_hier_report-odd",
        PRINTABLE_HIER_REPORT_EVEN: "printable_hier_report-even",
        PRINTABLE_HIER_REPORT_HEADER: "printable_hier_report-header",
        PRINTABLE_HIER_REPORT_PROGRESS_HEADER: "printable_hier_report-header-progress",
        PRINTABLE_HIER_REPORT_FOOTER: "printable_hier_report-footer",
        PRINTABLE_HIER_REPORT_PROGRESS: "printable_hier_report-progress",
        PRINTABLE_HIER_REPORT_CHECKS: "printable_hier_report-checks",
        PRINTABLE_HIER_REPORT_MISTAKES: "printable_hier_report-mistakes",
        PRINTABLE_HIER_REPORT_ERRORS: "printable_hier_report-errors",
        PRINTABLE_HIER_REPORT_PAGE_SCORE: "printable_hier_report-page-score",
        PRINTABLE_HIER_REPORT_PAGE_MAX_SCORE: "printable_hier_report-page-max-score",
        PRINTABLE_HIER_REPORT_PAGE_NON_MAX_SCORE: "printable_hier_report-page-non-max-score",
        PRINTABLE_HIER_REPORT_SCORE_DISABLED_ROW: "printable_hier_report-score-disabled-row",
        PRINTABLE_HIER_REPORT_INDENT: "printable_hier_report-indent",
    };

    function getTextVoiceObject (text, lang) {return {text: text, lang: lang};}

    presenter.ERROR_MESSAGES = {
        EXPAND_DEPTH_NOT_NUMERIC: "Depth of expand is not proper",

        C01: "Wrong classes name format",
        C02: "Class names has to be separated by new line",

        D01: "Values in Disable score on pages property should be numeric and non empty",
        D02: "Values in Disable score on pages property should be greater than 0",
        D03: "Values in Disable score on pages property should be unique",

        P01: "Values in Disable pages property should be numeric and non empty",
        P02: "Values in Disable pages property should be greater than 0",
        P03: "Values in Disable pages property should be unique",

        A01: "There should be at least one item in Alternative Page Names property",
        A02: "Values in Alternative Page Number property should be greater than 0"
    };

    function returnErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    function returnCorrectObject(v) { return { isValid: true, value: v }; }

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                if (substitutions.hasOwnProperty(key)) {
                    messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
                }
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }
        presenter.$view.html(errorContainer);
    };

    presenter.setPlayerController = function (controller) {
        presentationController = controller;
    };

    /**
     * @param controller (PrintableController)
     */
    presenter.setPrintableController = function (controller) {
        printableController = controller;
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    function addHeader(configuration, $view) {
        var labels = configuration.labels;
        var $row = generateHeaderRow();

        addTitleCell($row, labels.title);
        if (configuration.showResults) {
            addResultsHeaderCell($row, labels.results);
        }
        if (configuration.showChecks) {
            addChecksCell($row, labels.checks);
        }
        if (configuration.showMistakes) {
            addMistakesCell($row, labels.mistakes);
        }
        if (configuration.showErrors) {
            addErrorsCell($row, labels.errors);
        }
        if (configuration.showPageScore) {
            addPageScoreCell($row, labels.pageScores);
        }
        if (configuration.showMaxScoreField) {
            addMaxScoreAwardCell($row, labels.maxScoreAward);
        }

        $row.prependTo($view);
    }

    function addFooter(configuration, $view) {
        var lessonScore = getLessonScore();
        var $row = generateFooterRow();

        addTitleCell($row, configuration.labels.total);
        if (configuration.showResults) {
            addResultsFooterCell($row);
        }
        if (configuration.showChecks) {
            addChecksCell($row, lessonScore.checkCount);
        }
        if (configuration.showMistakes) {
            addMistakesCell($row, lessonScore.mistakeCount);
        }
        if (configuration.showErrors) {
            addErrorsCell($row, lessonScore.errorCount);
        }
        if (configuration.showPageScore) {
            addPageScoreFooterCell($row, lessonScore);
        }
        if (configuration.showMaxScoreField) {
            addMaxScoreAwardCell($row, undefined);
        }

        $row.appendTo($view);
    }

    function generateHeaderRow() {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_HEADER
            : CSS_CLASSES.HIER_REPORT_HEADER;
        return generateRow(className);
    }

    function generateFooterRow() {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_FOOTER
            : CSS_CLASSES.HIER_REPORT_FOOTER;
        return generateRow(className);
    }

    function generateRow(className) {
        var $row = $(document.createElement('tr'));
        $row.addClass(className);
        return $row;
    }

    function addTitleCell($row, title) {
        addCell($row, title);
    }

    function addResultsHeaderCell($row, header) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PROGRESS_HEADER
            : CSS_CLASSES.HIER_REPORT_PROGRESS;
        addCell($row, header, className);
    }

    function addChecksCell($row, checks) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_CHECKS
            : CSS_CLASSES.HIER_REPORT_CHECKS;
        addCell($row, checks, className);
    }

    function addMistakesCell($row, mistakes) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_MISTAKES
            : CSS_CLASSES.HIER_REPORT_MISTAKES;
        addCell($row, mistakes, className);
    }

    function addErrorsCell($row, errors) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_ERRORS
            : CSS_CLASSES.HIER_REPORT_ERRORS;
        addCell($row, errors, className);
    }

    function addPageScoreCell($row, pageScore) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PAGE_SCORE
            : CSS_CLASSES.HIER_REPORT_PAGE_SCORE;
        addCell($row, pageScore, className);
    }

    function addMaxScoreAwardCell($row, maxScoreAward) {
        const className = maxScoreAward
            ? (isInPrintableStateMode()
                ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PAGE_MAX_SCORE
                : CSS_CLASSES.HIER_REPORT_PAGE_MAX_SCORE)
            : undefined;
        addCell($row, maxScoreAward, className);
    }

    function addNameCell($row, node) {
        let $nameCell = generateTextWrapper(node.getName(), node.isChapter(), node.getId());
        $nameCell.appendTo($row);
    }

    function generateTextWrapper(text, isChapter, pageId) {
        var $textWrapper = $(document.createElement('div'));
        $textWrapper.addClass(CSS_CLASSES.TEXT_WRAPPER);
        var innerHTML = text;
        if (!isChapter &amp;&amp; !isInPrintableStateMode()) {
            innerHTML = generatePageLinks(text, isChapter, pageId);
        }
        $textWrapper.html(innerHTML);

        var $element = $(document.createElement('td'));
        $element.append($textWrapper);

        return $element;
    }

    function generatePageLinks(text, isChapter, pageId) {
        return $("&lt;a&gt;&lt;/a&gt;").html(text).attr('href', '#').attr('data-page-id', pageId);
    }

    function addCell($row, value, className) {
        var $cell = $(document.createElement('td'));
        if (className !== undefined) {
            $cell.addClass(className);
        }
        if (value !== undefined) {
            $cell.html(value);
        }
        $cell.appendTo($row);
    }

    function addResultsFooterCell($row) {
        var calculateScore = !(isPreviewConsideringPrintableState() || isInPrintableEmptyStateMode());

        createProgressCell($row, {
            score: calculateScore ? presenter.calculateLessonScore() : 0,
            count: 1
        }, undefined, undefined);
    }

    function isPreviewConsideringPrintableState() {
        return isInPrintableStateMode()
           ? printableController.isPreview()
           : presenter.isPreview;
    }

    function addPageScoreFooterCell($cell, lessonScore) {
        var $separator = generateSeparator();
        var content = lessonScore.score + $separator[0].outerHTML + lessonScore.maxScore;
        addPageScoreCell($cell, content);
    }

    function addEmptyRow($view, node, index, parentIndex) {
        var configuration = getConfiguration();
        var isInPrintableState = isInPrintableStateMode();
        var $row = $(document.createElement('tr'));
        $row.appendTo($view.find('table'));
        $row.addClass(configuration.classes[index % configuration.classes.length]);

        var nodeClassName = isInPrintableState
          ? CSS_CLASSES.PRINTABLE_HIER_REPORT_NODE + '-' + index
          : "treegrid-" + index;
        $row.addClass(nodeClassName);

        if (parentIndex != null) {
            var parentNodeClassName = isInPrintableState
              ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PARENT + '-' + parentIndex
              : "treegrid-parent-" + parentIndex
            $row.addClass(parentNodeClassName);
        }

        var isOdd = index % 2 &gt; 0;
        var typeClassName = node.isChapter()
          ? getRowChapterClassName()
          : getRowPageClassName(isOdd);
        $row.addClass(typeClassName);

        return $row;
    }

    function getRowChapterClassName() {
        return isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_CHAPTER
            : CSS_CLASSES.HIER_REPORT_CHAPTER;
    }

    function  getRowPageClassName(isOdd) {
        if (isOdd) {
            return isInPrintableStateMode()
              ? CSS_CLASSES.PRINTABLE_HIER_REPORT_ODD
              : CSS_CLASSES.HIER_REPORT_ODD;
        }
        return isInPrintableStateMode()
          ? CSS_CLASSES.PRINTABLE_HIER_REPORT_EVEN
          : CSS_CLASSES.HIER_REPORT_EVEN;
    }

    function createProgressCell($row, score, index, isChapter) {
        var isInPrintableState = isInPrintableStateMode();
        var $progressCell = $(document.createElement('td'));
        $progressCell.appendTo($row);

        var progressCellClassName = isInPrintableState
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PROGRESS
            : CSS_CLASSES.HIER_REPORT_PROGRESS;
        $progressCell.addClass(progressCellClassName);

        var percent = Math.round(score.score / score.count * 100);
        var innerHTML = percent + "%";

        if (isInPrintableState) {
            $progressCell.html(innerHTML);
        } else {
            var $progressbar = $(document.createElement('div'));
            $progressbar.attr("id", "progressbar-" + index);
            $progressbar.addClass(CSS_CLASSES.HIER_REPORT_PROGRESSBAR);
            $progressbar.appendTo($progressCell);

            var $progressInfo = $(document.createElement('div'));
            $progressInfo.attr("style", "float: right");
            $progressInfo.html(innerHTML);
            $progressInfo.appendTo($progressCell);

            if (!isChapter) {
                $progressbar.progressbar({
                    value: Math.round(score.score * 100),
                    max: 100
                });
            }
        }
    }

    presenter.isPageVisited = function (pageId) {
        if (isInPrintableStateMode()) {
            return presenter.isVisitedInPrintableMode(pageId);
        }

        if (!presenter.configuration.excludeUnvisitedPages) {
            return true;
        }

        return presentationController.getPresentation().getPageById(pageId).isVisited();
    };

    presenter.isVisited = function (pageId) {
        if (isInPrintableStateMode()) {
            return presenter.isVisitedInPrintableMode(pageId);
        }

        return presentationController.getPresentation().getPageById(pageId).isVisited();
    };

    presenter.isVisitedInPrintableMode = function (pageId) {
        if (printableController.isPreview()) {
            return false;
        }

        if (isInPrintableEmptyStateMode()) {
            return false;
        }

        return printableController.getContentInformation().find(x =&gt; x.id === pageId).isVisited === "true";
    };

    presenter.getPageScaledScore = function(maxScore, score, isChapter, pageID) {
        if (isInPrintableEmptyStateMode()) {
            return 0;
        }

        if (maxScore) {
            return score / maxScore;
        }

        const isPreview = isPreviewConsideringPrintableState();
        if (!isPreview &amp;&amp; !isChapter) {
            return presenter.isPageVisited(pageID) ? 1 : 0;
        }
        return 0;
    };

    function addScoreCellsWhenEnabledScoring($row, node, index) {
        const configuration = getConfiguration();
        let score = getScoreByPageIdForScoreCell(node.getId());

        if (!node.isChapter()) {
            presenter.updateScaledScore(score, node.getId());
        }

        if (configuration.showResults) {
            createProgressCell($row, {score: score.scaledScore, count: 1}, index, node.isChapter());
        }
        if (configuration.showChecks) {
            addChecksCell($row, score.checkCount);
        }
        if (configuration.showMistakes) {
            addMistakesCell($row, score.mistakeCount);
        }
        if (configuration.showErrors) {
            addErrorsCell($row, score.errorCount);
        }
        if (configuration.showPageScore) {
            addPageScoreRowCell($row, score, node.getId());
        }
        if (configuration.showMaxScoreField) {
            addMaxScoreAwardRowCell($row, score, node.getId());
        }

        updateLessonScore(score, node.getId(), node.isChapter());
    }

    function addScoreCellsWhenDisabledScoring($row) {
        var c = getConfiguration();
        var columns = [c.showResults, c.showChecks, c.showMistakes, c.showErrors, c.showPageScore, c.showMaxScoreField].filter(function(a) { return a }).length;
        var $colspan = $("&lt;td colspan='" + columns + "'&gt;&lt;/td&gt;");
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_SCORE_DISABLED_ROW
            : CSS_CLASSES.HIER_REPORT_SCORE_DISABLED_ROW;
        $colspan.addClass(className);
        $colspan.appendTo($row);
    }

    function getScoreByPageIdForScoreCell(pageId) {
        if (isPreviewConsideringPrintableState()) {
            return createEmptyScore(pageId);
        }

        if (isInPrintableStateMode()) {
            return getPrintablePageScoreById(pageId);
        }
        return presentationController.getScore().getPageScoreById(pageId);
    }

    function getPrintablePageScoreById(pageId) {
        var score = printableController.getScore();
        if (score === null) {
            return createEmptyScore(pageId);
        }

        if (score.hasOwnProperty(pageId)) {
            return {...score[pageId]}
        }
        return createEmptyScore(pageId);
    }

    function addPageScoreRowCell($view, score, pageId) {
        var innerHTML = createInnerHTMLForScoreCell(score, pageId);
        addPageScoreCell($view, innerHTML);
    }

    function addMaxScoreAwardRowCell($view, score, pageId) {
        var isInPrintableState = isInPrintableStateMode();
        var $cell = $(document.createElement('td'));

        var isMaxScore = score.maxScore !== 0 &amp;&amp; score.score === score.maxScore;
        const className = isMaxScore
            ? (isInPrintableState
                ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PAGE_MAX_SCORE
                : CSS_CLASSES.HIER_REPORT_PAGE_MAX_SCORE)
            : (isInPrintableState
                ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PAGE_NON_MAX_SCORE
                : CSS_CLASSES.HIER_REPORT_PAGE_NON_MAX_SCORE);
        $cell.addClass(className);

        if (!isInPrintableState) {
            var $element = generateMaxScoreLinks(pageId, isMaxScore);
            $($cell).append($element);
        }
        $view.append($cell);
    }

    function generateMaxScoreLinks(pageId, isMaxScore) {
        var $element = $(document.createElement('td'));
        const className = isMaxScore
            ? CSS_CLASSES.HIER_REPORT_PAGE_MAX_SCORE
            : CSS_CLASSES.HIER_REPORT_PAGE_NON_MAX_SCORE;
        $element.addClass(className);

        var $link = $("&lt;a&gt;&lt;/a&gt;").attr('href', '#').attr('data-page-id', pageId);
        $link.append($element);
        return $link;
    }

    function parsePrintableControllerLessonsScoresToValidForm() {
        if (isInPrintableShowResultsStateMode()) {
            var scores = printableController.getScore();
            for (const [key, value] of Object.entries(scores)) {
                parseValuesToIntInDict(value);
            }
        }
    }

    function parseValuesToIntInDict(dictionary) {
        for (const [key, value] of Object.entries(dictionary)) {
            dictionary[key] = parseInt(dictionary[key]);
        }
    }

    function generateSeparator() {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_SEPARATOR
            : CSS_CLASSES.HIER_REPORT_SEPARATOR;
        var $separator = $('&lt;span&gt;&lt;/span&gt;');
        $separator.html('/');
        $separator.addClass(className);
        return $separator;
    }

    function checkIfChapterHasChildren () {
        presenter.$view.find(".hier_report-chapter").each(function () {
           if(!$(this).hasClass('treegrid-collapsed') &amp;&amp; !$(this).hasClass('treegrid-expanded')) {
               $(this).remove();
           }
        });
    }

    function checkIfPageEnabled () {
        const configuration = getConfiguration();
        if (configuration.enablePages != '' &amp;&amp; configuration.enablePages != undefined){
            const realIndex = parseInt(getRelativeIndex() - getChapterIndex(), 10);
            return configuration.enablePages.indexOf(realIndex) &gt; -1;
        }
        return true;
    }

    function addRow($view, node, index, parentIndex, isPreview) {
        const $row = addEmptyRow($view, node, index, parentIndex);

        if (!isPreview) {
            updateNodeNameByAlternativeName(node);
        }
        addNameCell($row, node);

        if (isScoringInPageEnabled(getAbsoluteIndex())) {
            addScoreCellsWhenEnabledScoring($row, node, index);
        } else {
            addScoreCellsWhenDisabledScoring($row);
        }
        return $row;
    }

    function updateNodeNameByAlternativeName (node) {
        const alternativeNameIndex = node.isChapter() ? getChapterIndex() : getPageIndex();
        const alternativeName = presenter.findAlternativeName(alternativeNameIndex, node.isChapter());
        node.setName(alternativeName || node.getName());
    }

    presenter.findAlternativeName = function (index, isChapter){
        let result = undefined;
        const configuration = getConfiguration();
        const alternativeTitles = configuration.alternativePageTitles;

        alternativeTitles.forEach(alternativeTitle =&gt; {
            if ((alternativeTitle.alternativePageNumber - 1) === index &amp;&amp; alternativeTitle.alternativePageIsChapter === isChapter){
                result = alternativeTitle.alternativePageName;
            }
        });
        return result;
    };

    function updateChapterRow($row, chapterIndex, score, pageId) {
        const hasChildren = score.pageCount &gt; 0;
        const configuration = getConfiguration();

        if (configuration.showResults) {
            updateChapterRowResultsCell($row, chapterIndex, score, hasChildren);
        }
        if (configuration.showChecks) {
            updateChapterRowChecksCell($row, score.checkCount, hasChildren);
        }
        if (configuration.showMistakes) {
            updateChapterRowMistakesCell($row, score.mistakeCount, hasChildren);
        }
        if (configuration.showErrors) {
            updateChapterRowErrorsCell($row, score.errorCount, hasChildren);
        }
        if (configuration.showPageScore) {
            updateChapterRowScoreCell($row, score, hasChildren, pageId);
        }
    }

    function updateChapterRowResultsCell($row, pageIndex, score, hasChildren) {
        if (isInPrintableStateMode()) {
            updateChapterRowResultsPrintableCell($row, score, hasChildren);
        } else {
            updateChapterRowResultsRunCell($row, pageIndex, score, hasChildren);
        }
    }

    function updateChapterRowResultsPrintableCell($row, score, hasChildren) {
        const className = CSS_CLASSES.PRINTABLE_HIER_REPORT_PROGRESS;
        const value = hasChildren ? score.weightedScaledScoreNumerator / score.weightedScaledScoreDenominator : 0;
        const percent = (Math.round(value * 100) || 0) + '%';
        updateCell($row, percent, hasChildren, className);
    }

    function updateChapterRowResultsRunCell($row, pageIndex, score, hasChildren) {
        var percent = Math.round((score.weightedScaledScoreNumerator / score.weightedScaledScoreDenominator) * 100) || 0;
        var $progressbar = $($row.find("#progressbar-" + pageIndex));
        if (hasChildren) {
            const value = score.weightedScaledScoreNumerator / score.weightedScaledScoreDenominator;
            $progressbar.progressbar({value: Math.round(value * 100), max: 100});
            $progressbar.closest("div").next().html(percent + '%');
        } else {
            $progressbar.closest("div").next().html('-').attr('style', '');
        }
    }

    function updateChapterRowChecksCell($row, checks, hasChildren) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_CHECKS
            : CSS_CLASSES.HIER_REPORT_CHECKS;
        updateCell($row, checks, hasChildren, className);
    }

    function updateChapterRowMistakesCell($row, mistakes, hasChildren) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_MISTAKES
            : CSS_CLASSES.HIER_REPORT_MISTAKES;
        updateCell($row, mistakes, hasChildren, className);
    }

    function updateChapterRowErrorsCell($row, errors, hasChildren) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_ERRORS
            : CSS_CLASSES.HIER_REPORT_ERRORS;
        updateCell($row, errors, hasChildren, className);
    }

    function updateChapterRowScoreCell($row, score, hasChildren, pageId) {
        const className = isInPrintableStateMode()
            ? CSS_CLASSES.PRINTABLE_HIER_REPORT_PAGE_SCORE
            : CSS_CLASSES.HIER_REPORT_PAGE_SCORE;
        const innerHTML = createInnerHTMLForScoreCell(score, pageId);
        updateCell($row, innerHTML, hasChildren, className);
    }

    presenter.createInnerHTMLForScoreCell = function (score, pageId) {
        return createInnerHTMLForScoreCell(score, pageId);
    }

    function createInnerHTMLForScoreCell (score, pageId) {
        const $separator = generateSeparator();

        if (isNaN(score.maxScore)) {
            return getConfiguration().labels.unvisitedPageScore;
        }

        if (isInPrintableStateMode() &amp;&amp; !getConfiguration().excludeUnvisitedPages &amp;&amp; score.maxScore === 0) {
            return score.score + '/' + score.maxScore;
        }

        if (score.maxScore === 0 &amp;&amp; !getConfiguration().excludeUnvisitedPages &amp;&amp; pageId &amp;&amp; pageId != "chapter" &amp;&amp; presentationController) {
            const maxScore = presentationController.getPresentation().getPageById(pageId).getModulesMaxScore();
            presenter.totalScore += maxScore;

            return score.score + $separator[0].outerHTML + maxScore;
        }

        if (score.score === 0 &amp;&amp; score.maxScore === 0 &amp;&amp; score.scaledScore === 0) {
            return getConfiguration().labels.unvisitedPageScore;
        }

        if (pageId != "chapter") {
            presenter.totalScore += score.maxScore;
        }

        return score.score + $separator[0].outerHTML + score.maxScore;
    }

    function updateCell($row, value, hasChildren, searchClassName) {
        const innerHTML = hasChildren ? value : '-';
        $row.find(`.${searchClassName}`).html(innerHTML);
    }

    presenter.calculateLessonScore = function () {
        let lessonScore = getLessonScore();
        if (lessonScore.weightedScaledScoreDenominator === 0) {
            return 0;
        }

        return Math.round((lessonScore.weightedScaledScoreNumerator / lessonScore.weightedScaledScoreDenominator) * 100) / 100;
    };

    function updateLessonScore (pageScore, pageId, isChapter) {
        pageScore.weightedScaledScoreNumerator = 0;
        pageScore.weightedScaledScoreDenominator = 0;

        let lessonScore = getLessonScore();
        updateChapterOrLessonScore(lessonScore, pageScore, pageId, isChapter);
    }

    function updateChapterScore (chapterScore, score, pageId, isChapter) {
        updateChapterOrLessonScore(chapterScore, score, pageId, isChapter);
    }

    /**
     * Update chapter's/lesson's score by given score. Only to execute when scoring is enabled.
     *  @method updateChapterOrLessonScore
     *  @param {Object} mainScore Lesson or chapter scores to be updated
     *      @param {Number} mainScore.errorCount
     *      @param {Number} mainScore.checkCount
     *      @param {Number} mainScore.mistakeCount
     *      @param {Number} mainScore.maxScore
     *      @param {Number} mainScore.score
     *      @param {Number} mainScore.pageCount
     *      @param {Number} mainScore.weightedScaledScoreNumerator
     *      @param {Number} mainScore.weightedScaledScoreDenominator
     *  @param {Object} score Chapter (representation of scores for more than one page) or page scores to update
     *  @param {String} pageId Page unique ID
     *  @param {boolean} isChapter: If score is a chapter (representation of scores for more than one page) score
     */
    function updateChapterOrLessonScore (mainScore, score, pageId, isChapter) {
        if (isChapter &amp;&amp; score.pageCount) {
            mainScore.pageCount += score.pageCount;
        } else {
            mainScore.pageCount += 1;
        }

        if (isInPrintableEmptyStateMode() &amp;&amp; !getConfiguration().excludeUnvisitedPages) {
            mainScore.maxScore += score.maxScore;
        }

        if (isInPrintableEmptyStateMode() || isPreviewConsideringPrintableState()) {
            return;
        }

        mainScore.errorCount += score.errorCount;
        mainScore.checkCount += score.checkCount;
        mainScore.mistakeCount += score.mistakeCount;
        mainScore.maxScore += score.maxScore;
        mainScore.score += score.score;

        _updateWeightedScaledScoreParts(mainScore, score, pageId, isChapter);
    }

    function _updateWeightedScaledScoreParts (mainScore, score, pageId, isChapter) {
        if (isChapter) {
            mainScore.weightedScaledScoreNumerator += score.weightedScaledScoreNumerator;
            mainScore.weightedScaledScoreDenominator += score.weightedScaledScoreDenominator;
            return;
        }

        let weight = 1;
        if (presentationController &amp;&amp; !getConfiguration().excludeUnvisitedPages &amp;&amp; getConfiguration().isWeightedArithmeticMean) {
            weight = presentationController.getPresentation().getPageById(pageId).getPageWeight();
        } else if (getConfiguration().isWeightedArithmeticMean &amp;&amp; score.weight !== 0) {
            weight = score.weight;
        }

        if (isInPrintableStateMode() &amp;&amp; getConfiguration().isWeightedArithmeticMean) {
            const printablePageWeight = printableController.getContentInformation().find(x =&gt; x.id === pageId).pageWeight;
            weight = printablePageWeight ? +printablePageWeight : 1;
        }

        if (score.maxScore) {
            mainScore.weightedScaledScoreNumerator += score.score * weight / score.maxScore;
            mainScore.weightedScaledScoreDenominator += weight;
        } else if (presenter.isPageVisited(pageId) &amp;&amp; getConfiguration().excludeUnvisitedPages) {
            mainScore.weightedScaledScoreNumerator += weight;
            mainScore.weightedScaledScoreDenominator += weight;
        } else if (presenter.isVisited(pageId) &amp;&amp; !getConfiguration().excludeUnvisitedPages) {
            mainScore.weightedScaledScoreNumerator += weight;
            mainScore.weightedScaledScoreDenominator += weight;
        } else if (!getConfiguration().excludeUnvisitedPages) {
            mainScore.weightedScaledScoreDenominator += weight;
        }
    }

    presenter.updateScaledScore = function (score, pageId) {
        if (isInPrintableEmptyStateMode() || isPreviewConsideringPrintableState()) {
            score.scaledScore = 0
            return;
        }

        if (presentationController &amp;&amp; !presentationController.getPresentation().getPageById(pageId).isVisited() &amp;&amp; !getConfiguration().excludeUnvisitedPages) {
            score.scaledScore = 0;
            return;
        }

        if (score.maxScore) {
            score.scaledScore = score.score / score.maxScore;
        } else if (presenter.isPageVisited(pageId)) {
            score.scaledScore = 1;
        } else {
            score.scaledScore = 0;
        }
    }

    presenter.createPreviewTree = function() {
        let contentInformation = generatePreviewContentInformation();
        let rootRepresentation = createDeepNodeRepresentationOfContentInformation(contentInformation);

        let chapterScore = createEmptyScore();
        let $view = $("#" + presenter.treeID);
        for (let i = 0; i &lt; rootRepresentation.size(); i++) {
            addRow($view, rootRepresentation.get(i), i, contentInformation[i].parentId, true);
        }
        return chapterScore;
    };

    presenter.createTree = function($view, parentNodeRepresentation, parentIndex) {
        let pageScore = createEmptyScore();
        let chapterScore = createEmptyScore();

        for (let i = 0; i &lt; parentNodeRepresentation.size(); i++) {
            let nodeRepresentation = parentNodeRepresentation.get(i);

            if (!nodeRepresentation.isChapter() &amp;&amp; !nodeRepresentation.isReportable()) {
                updateIndexesWhenNotReportablePage(nodeRepresentation);
                continue;
            }

            if (nodeRepresentation.isChapter() || checkIfPageEnabled()) {
                addRow($view, nodeRepresentation, getRelativeIndex(), parentIndex, false);
            }

            const currentNodeRelativeIndex = getRelativeIndex();
            const currentNodeAbsoluteIndex = getAbsoluteIndex();
            updateIndexesWhenChapterOrReportablePage(nodeRepresentation.isChapter());

            if (nodeRepresentation.isChapter()) {
                const childChapterScore = presenter.createTree($view, nodeRepresentation, currentNodeRelativeIndex);
                let $row = findChapterElement($view, currentNodeRelativeIndex);
                updateChapterRow($row, currentNodeRelativeIndex, childChapterScore, nodeRepresentation.getId());
                pageScore = childChapterScore;
            } else {
                pageScore = getScoreByPageIdForScoreCell(nodeRepresentation.getId());
            }

            if (isScoringInPageEnabled(currentNodeAbsoluteIndex) || nodeRepresentation.isChapter()) {
                updateChapterScore(chapterScore, pageScore, nodeRepresentation.getId(), nodeRepresentation.isChapter());
            }
        }

        return chapterScore;
    };

    function findChapterElement($view, chapterIndex) {
        return isInPrintableStateMode()
            ? $($view.find(`.${CSS_CLASSES.PRINTABLE_HIER_REPORT_NODE}-` + chapterIndex))
            : $($view.find(".treegrid-" + chapterIndex));
    }

    function isScoringInPageEnabled(index) {
        return getConfiguration().disabledScorePages.indexOf(index) === -1;
    }

    function updateIndexesWhenNotReportablePage() {
        const configuration = getConfiguration();
        increaseAbsoluteIndex();

        if (configuration.enablePages != '' &amp;&amp; configuration.enablePages != undefined) {
            increaseRelativeIndex();
        }
        increasePageIndex();
    }

    function updateIndexesWhenChapterOrReportablePage(isChapter) {
        increaseAbsoluteIndex();
        increaseRelativeIndex();

        if (isChapter) {
            increaseChapterIndex();
        } else {
            increasePageIndex();
        }
    }

    function getAbsoluteIndex() {
        return absoluteIndex;
    }

    function getRelativeIndex() {
        return relativeIndex;
    }

    function getPageIndex() {
        return pageIndex;
    }

    function getChapterIndex() {
        return chapterIndex;
    }

    function increaseAbsoluteIndex() {
        absoluteIndex += 1;
    }

    function increaseRelativeIndex() {
        relativeIndex += 1;
    }

    function increasePageIndex() {
        pageIndex += 1;
    }

    function increaseChapterIndex() {
        chapterIndex += 1;
    }

    function handleMouseClickActions() {
        var commander = presentationController.getCommands(),
            $report = presenter.$view.find('.hier_report tr');

        $report.find('td a').each(function () {
            $(this).click(function (event) {
                event.preventDefault();
                event.stopPropagation();
                commander.gotoPageId($(this).attr('data-page-id'));
            });
        });

        $report.find('.treegrid-expander').each(function () {
            $(this).click(function (event) {
                event.preventDefault();
                event.stopPropagation();
            });
        });
    }

    function expandTree(level) {
        $('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function () {
            if ($(this).treegrid('getDepth') &lt; level) {
                $(this).treegrid('expand');
            }
        });
    }

    function saveTreeState() {
        var state = [];
        $('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function () {
            state.push($(this).treegrid('isExpanded'));
        });
        return state;
    }

    function restoreTreeState(state) {
        $('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function (index) {
            $(this).treegrid(state[index] ? 'expand' : 'collapse');
        });
    }

    presenter.getState = function () {
        return JSON.stringify({
            'treeState': saveTreeState(),
            'isVisible': presenter.configuration.isVisible
        });
    };

    presenter.setState = function (stateString) {
        var state = JSON.parse(stateString);

        restoreTreeState(state.treeState);

        presenter.setVisibility(state.isVisible);
        presenter.configuration.isVisible = state.isVisible;
    };

    function parseClasses(classes_text) {
        function isValidClassName(class_name) {
            return /^[a-z_-][a-z\d_-]*$/i.test(class_name);
        }

        if (ModelValidationUtils.isStringEmpty(classes_text)) {
            return returnCorrectObject([]);
        }

        var classes = classes_text.split('\n');
        for (var i=0; i&lt;classes.length; i++) {
            if (classes[i].indexOf(' ') !== -1) {
                return returnErrorObject("C02");
            }

            if (!isValidClassName(classes[i])) {
                return returnErrorObject("C01");
            }
        }

        return returnCorrectObject(classes);
    }

    function parseScoreDisable(pages_text, disabledType) {
        if (ModelValidationUtils.isStringEmpty(pages_text)) {
            return returnCorrectObject([]);
        }

        var i;

        var pages = pages_text.split(';');
        for (i=0; i&lt;pages.length; i++) {
            var numberObject = ModelValidationUtils.validateInteger(pages[i]);
            if (!numberObject.isValid) {
                if(disabledType == 'score'){
                    return returnErrorObject("D01");
                } else if (disabledType == 'pages') {
                    return returnErrorObject("P01");
                }
            }

            pages[i] = numberObject.value - 1; // indexing from 0

            if (pages[i] &lt; 0) {
                if(disabledType == 'score'){
                    return returnErrorObject("D02");
                } else if (disabledType == 'pages') {
                    return returnErrorObject("P02");
                }
            }
        }

        for (i=1; i&lt;pages.length; i++) {
            if (pages.sort()[i] === pages.sort()[i-1]) {
                if(disabledType == 'score'){
                    return returnErrorObject("D03");
                } else if (disabledType == 'pages') {
                    return returnErrorObject("P03");
                }
            }
        }

        return returnCorrectObject(pages.sort());
    }

    presenter.validateAlternativePageTitles = function (listOfPages) {
        var validatedList = [];

        if (listOfPages.length === undefined || listOfPages.length === 0) {
            return returnErrorObject('A01');
        }

        for (var i = 0; i &lt; listOfPages.length; i++) {
             var alternativePageName = listOfPages[i].alternativePageName;
             var isChapter = ModelValidationUtils.validateBoolean(listOfPages[i].alternativePageIsChapter);

             var alternativePageNumber = "";

             if (!ModelValidationUtils.isStringEmpty(listOfPages[i].alternativePageNumber)) {
                var alternativePageNumberObject = ModelValidationUtils.validatePositiveInteger(listOfPages[i].alternativePageNumber);
                if (!alternativePageNumberObject.isValid) {
                    return returnErrorObject('A02');
                }

                alternativePageNumber = alternativePageNumberObject.value;
             }

             validatedList[i] = {
                 alternativePageName: alternativePageName,
                 alternativePageNumber: alternativePageNumber,
                 alternativePageIsChapter: isChapter
             };
        }

        return returnCorrectObject(validatedList);
    };

    presenter.upgradeAlternativePageNamesProperty = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (model["alternativePageTitles"] === undefined) {
            upgradedModel["alternativePageTitles"] = [{
                alternativePageNumber: "",
                alternativePageName: "",
                alternativePageIsChapter: "false"
            }];
        }
        return upgradedModel;
    };

    presenter.upgradeTextToSpeechSupport = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel['speechTexts'] === undefined) {
            upgradedModel['speechTexts'] = {
                Expanded: {Expanded: "Expanded"},
                Collapsed: {Collapsed: "Collapsed"},
                Results: {Results: "Results"},
                Checks: {Checks: "Checks"},
                Mistakes: {Mistakes: "Mistakes"},
                Errors: {Errors: "Errors"},
                Score: {Score: "Score"},
                OutOf: {OutOf: "Out of"},
                Total: {Total: "Total"}
            };
        }

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] = "";
        }
        return upgradedModel;
    };

    presenter.upgradeIsWeightedArithmeticMean = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel["isWeightedArithmeticMean"] === undefined) {
            upgradedModel["isWeightedArithmeticMean"] = "False";
        }

        return upgradedModel;
    }

    presenter.upgradeExcludeUnvisitedPages = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel["excludeUnvisitedPages"] === undefined) {
            upgradedModel["excludeUnvisitedPages"] = "False";
        }

        return upgradedModel;
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeAlternativePageNamesProperty(model);
        upgradedModel = presenter.upgradeTextToSpeechSupport(upgradedModel);
        upgradedModel = presenter.upgradeIsWeightedArithmeticMean(upgradedModel);
        upgradedModel = presenter.upgradeExcludeUnvisitedPages(upgradedModel);

        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            expanded:  'Expanded',
            collapsed: 'Collapsed',
            results: 'Percentage results',
            checks: 'Number of checks',
            mistakes: 'Number of mistakes',
            errors: 'Number of errors',
            score: 'Score',
            outOf: 'out of',
            Total: 'Total'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            expanded:    getSpeechTextProperty(speechTexts['Expanded']['Expanded'], presenter.speechTexts.expanded),
            collapsed: getSpeechTextProperty(speechTexts['Collapsed']['Collapsed'], presenter.speechTexts.collapsed),
            results:  getSpeechTextProperty(speechTexts['Results']['Results'], presenter.speechTexts.results),
            checks:     getSpeechTextProperty(speechTexts['Checks']['Checks'], presenter.speechTexts.checks),
            mistakes:   getSpeechTextProperty(speechTexts['Mistakes']['Mistakes'], presenter.speechTexts.mistakes),
            errors:      getSpeechTextProperty(speechTexts['Errors']['Errors'], presenter.speechTexts.errors),
            score:        getSpeechTextProperty(speechTexts['Score']['Score'], presenter.speechTexts.score),
            outOf:        getSpeechTextProperty(speechTexts['OutOf']['OutOf'], presenter.speechTexts.outOf),
            total:        getSpeechTextProperty(speechTexts['Total']['Total'], presenter.speechTexts.total)
        };
    };

    presenter.validateModel = function (model) {
        presenter.setSpeechTexts(model['speechTexts']);
        var expandDepth = returnCorrectObject(0);

        if (model['expandDepth'].length &gt; 0) {
            expandDepth = ModelValidationUtils.validateInteger(model['expandDepth']);
            if (!expandDepth.isValid) {
                return returnErrorObject('EXPAND_DEPTH_NOT_NUMERIC');
            }
        }

        var validatedClasses = parseClasses(model["classes"]);
        if (!validatedClasses.isValid) {
            return returnErrorObject(validatedClasses.errorCode);
        }

        var validatedDisabledScorePages = parseScoreDisable(model["scoredisabled"], 'score');
        if (!validatedDisabledScorePages.isValid) {
            return returnErrorObject(validatedDisabledScorePages.errorCode);
        }

        var validatedEnablePages = parseScoreDisable(model["enablePages"], 'pages');
        if (!validatedEnablePages.isValid) {
            return returnErrorObject(validatedEnablePages.errorCode);
        }

        var validatedAlternativePageTitles = presenter.validateAlternativePageTitles(model["alternativePageTitles"]);
        if (!validatedAlternativePageTitles.isValid) {
            return returnErrorObject(validatedAlternativePageTitles.errorCode);
        }

        return {
            ID: model.ID,
            isValid: true,
            width: parseInt(model["Width"], 10),
            height: parseInt(model["Height"], 10),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            labels: {
                results: model['resultsLabel'],
                errors: model['errorsLabel'],
                checks: model['checksLabel'],
                mistakes: model['mistakesLabel'],
                total: model['totalLabel'],
                title: model['titleLabel'],
                pageScores: model['pageScoresLabel'] || "",
                maxScoreAward: model['maxScoreAwardLabel'] || "",
                unvisitedPageScore: model['unvisitedPageScoresLabel'] || ""
            },
            showResults: ModelValidationUtils.validateBoolean(model["results"]),
            showErrors: ModelValidationUtils.validateBoolean(model["errors"]),
            showChecks: ModelValidationUtils.validateBoolean(model["checks"]),
            showMistakes: ModelValidationUtils.validateBoolean(model["mistakes"]),
            showTotal: ModelValidationUtils.validateBoolean(model["total"]),
            expandDepth: expandDepth.value,
            classes: validatedClasses.value,
            showPageScore: ModelValidationUtils.validateBoolean(model["showpagescore"]),
            showMaxScoreField: ModelValidationUtils.validateBoolean(model["showmaxscorefield"]),
            disabledScorePages: validatedDisabledScorePages.value,
            enablePages: validatedEnablePages.value,
            alternativePageTitles: validatedAlternativePageTitles.value,
            langTag: model['langAttribute'],
            isWeightedArithmeticMean: ModelValidationUtils.validateBoolean(model["isWeightedArithmeticMean"]),
            excludeUnvisitedPages: ModelValidationUtils.validateBoolean(model["excludeUnvisitedPages"])
        };
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.initialize = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.isPreview = isPreview;
        initLessonScore();

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[presenter.configuration.errorCode]);
            return;
        }

        var padding_width = parseInt(presenter.$view.find('.hier_report').css('padding'));
        if(isNaN(padding_width)) padding_width = 0;
        var border_width = parseInt(presenter.$view.find('.hier_report').css('border-width'));
        if(isNaN(border_width)) border_width = 0;
        var actual_height = presenter.configuration.height + ( padding_width + border_width ) *2; // makes it so hier_report and the addons view are the same height

        presenter.$view.css('height',actual_height+'px');
        presenter.$view.find('.hier_report').attr("style", "height: " + presenter.configuration.height + "px");
        presenter.treeID = presenter.configuration.ID + (isPreview ? "Preview" : "");
        presenter.$view.find("div").first().attr('id', presenter.treeID);

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        var $table = $("#" + presenter.treeID).find('table');
        addHeader(presenter.configuration, $table);
        resetIndexes();
        if (isPreview) {
            presenter.createPreviewTree();
        } else {
            var $view = $("#" + presenter.treeID);
            var rootNode = createRunStructure();
            presenter.createTree($view, rootNode, null);
        }

        if (presenter.configuration.showTotal) {
            addFooter(presenter.configuration, $table);
        }

        $("#" + presenter.treeID).find('table').not('.hier_report-header').not('.hier_report-footer').treegrid({
            'initialState': 'collapsed',
            'expanderTemplate': '&lt;div class="treegrid-expander"&gt;&lt;/div&gt;'
        });

        expandTree(presenter.configuration.expandDepth);
        if (!isPreview) {
            handleMouseClickActions();
        }

        checkIfChapterHasChildren();
    };

    function initLessonScore() {
        var initLessonScore = createEmptyScore();

        if (isInPrintableStateMode()) {
            presenter.printableLessonScore = initLessonScore;
        } else {
            presenter.lessonScore = initLessonScore;
        }
    }

    function createEmptyScore(pageId) {
        const _maxScore = isInPrintableStateMode() &amp;&amp; !getConfiguration().excludeUnvisitedPages ? getMaxScoreForPrintableVersion(pageId) : 0;
        return {
            // Native score fields
            checkCount: 0,
            errorCount: 0,
            mistakeCount: 0,
            score: 0,
            maxScore: _maxScore,
            scaledScore: 0,
            weight: 0,

            // Score fields to calculate result
            pageCount: 0,
            weightedScaledScoreNumerator: 0,
            weightedScaledScoreDenominator: 0,
        };
    }

    function getMaxScoreForPrintableVersion(pageId) {
        const content = printableController.getContentInformation().find(x =&gt; x.id === pageId)
        return content ? +content.maxScore : 0;
    }

    function getLessonScore() {
        if (isInPrintableStateMode()) {
            presenter.printableLessonScore.maxScore = !getConfiguration().excludeUnvisitedPages ? presenter.totalScore : 0;
            return presenter.printableLessonScore;
        }

        if (!getConfiguration().excludeUnvisitedPages) {
            presenter.lessonScore.maxScore = presenter.totalScore;
            return presenter.lessonScore;
        }

        return presenter.lessonScore;
    }

    function getConfiguration() {
        if (isInPrintableStateMode())
            return presenter.printableConfiguration;
        return presenter.configuration;
    }

    function getCell(rowNumber, columnNumber) {
        var $cell = presenter.$view.find('tr:eq('+rowNumber+') &gt; td:eq('+columnNumber+')');
        if($cell.size()&gt;0){
            return $cell[0];
        }
        return null;
    }

    presenter.cellIsVisible = function(rowNumber, columnNumber) {
        var cell = getCell(rowNumber, columnNumber);
        return cell &amp;&amp; $(cell).is(':visible');
    };

    function getTableMaxHeight() {
            return presenter.$view.find('tr').size();
        }

        function getTableWidth() {
            return presenter.$view.find('tr:eq(0) &gt; td').size();
        }

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        event.preventDefault();
        presenter.shiftPressed = event.shiftKey;

        var keys = {
            ENTER: 13,
            ESCAPE: 27,
            SPACE: 32,
            ARROW_LEFT: 37,
            ARROW_UP: 38,
            ARROW_RIGHT: 39,
            ARROW_DOWN: 40,
            TAB: 9
        };

        function moveTo(newRow, newColumn) {
            if(!presenter.cellIsVisible(newRow,newColumn)) {
                return;
            }
            currentColumn = newColumn;
            currentRow = newRow;
            presenter.$view.find('.' + CSS_CLASSES.SELECTED_CELL).removeClass(CSS_CLASSES.SELECTED_CELL);
            var cell = getCell(currentRow, currentColumn);
            $(cell).addClass(CSS_CLASSES.SELECTED_CELL);
            scrollCellIntoView();

        }

        function scrollCellIntoView () {
            var cell = getCell(currentRow,currentColumn);
            var $cell = $(cell);
            var $hier_report = presenter.$view.find('.hier_report');
            var scrollPos = $hier_report.scrollTop();

            if($cell.outerHeight() + cell.offsetTop &gt; $hier_report.height() + scrollPos) {
                $hier_report.scrollTop($cell.height() + parseInt($cell.css('padding-top')) + cell.offsetTop - $hier_report.height() );
            } else if(cell.offsetTop &lt; scrollPos) {
                $hier_report.scrollTop(cell.offsetTop + parseInt($cell.css('padding-top')));
            }
        };

        function getNextVisibleRowNumber(rowNumber) {
            var newRow = rowNumber;
            var nRows = getTableMaxHeight();
            while (true) {
                newRow+=1;
                if(newRow&gt;=nRows) {
                    return rowNumber;
                }
                if(presenter.cellIsVisible(newRow,0)){
                    return newRow;
                }
            }
        }

        function getPrevVisibleRowNumber(rowNumber) {
            var newRow = rowNumber;
            while (true) {
                newRow-=1;
                if(newRow&lt;=0) {
                    return rowNumber;
                }
                if(presenter.cellIsVisible(newRow,0)){
                    return newRow;
                }
            }
        }

        var enter = function (){
            if(isShiftKeyDown) {
                return escape();
            }

            moveTo(currentRow,currentColumn);
            presenter.readCurrentRowAndCell();
        };

        var next_element = function (){
            moveTo(currentRow,currentColumn+1);
            presenter.readCurrentCell();
        };

        var previous_element = function (){
            moveTo(currentRow,currentColumn-1);
            presenter.readCurrentCell();
        };

        var next_row = function () {
            var newRow = getNextVisibleRowNumber(currentRow);
            moveTo(newRow,currentColumn);
            presenter.readCurrentRowAndCell();
        };

        var previous_row = function () {
            var newRow = getPrevVisibleRowNumber(currentRow);
            moveTo(newRow,currentColumn);
            presenter.readCurrentRowAndCell();
        };

        var space = function (){
            var $cell = $(getCell(currentRow,currentColumn));
            var $link = $cell.find('a');
            if ($link.size()&gt;0) {
                $link.trigger('click');
            } else {
                var $expand = $cell.find('.treegrid-expander');
                if ($expand.size()&gt;0) {
                    $expand.trigger("click");
                    if ($expand.hasClass("treegrid-expander-collapsed")) {
                        speak([getTextVoiceObject(presenter.speechTexts.collapsed,"")]);
                    } else if ($expand.hasClass("treegrid-expander-expanded")) {
                        speak([getTextVoiceObject(presenter.speechTexts.expanded,"")]);
                    }
                }
            }
        };

        var escape = function (){
            presenter.$view.find('.' + CSS_CLASSES.SELECTED_CELL).removeClass(CSS_CLASSES.SELECTED_CELL);
            currentColumn = 0;
            currentRow = 1;
        };

        function tabHandler() {
            var rowChange = false;
            var maxWidth = getTableWidth();

            var newColumn = currentColumn;
            var newRow = currentRow;

            if (isShiftKeyDown) {
                newColumn -= 1;
            } else {
                newColumn += 1;
            }

            if(newColumn&gt;=maxWidth) {
                newRow = getNextVisibleRowNumber(newRow);
                if(newRow!==currentRow) {
                    newColumn = 0;
                    rowChange=true;
                } else {
                    newColumn = maxWidth-1;
                }
            } else if(newColumn&lt;0) {
                newRow = getPrevVisibleRowNumber(newRow);
                 if(newRow!==currentRow) {
                    newColumn = maxWidth-1;
                    rowChange=true;
                } else {
                    newColumn = 0;
                }
            }

            moveTo(newRow, newColumn);
            if(rowChange) {
                presenter.readCurrentRowAndCell();
            } else {
                presenter.readCurrentCell();
            }

        }

        var mapping = {};
        mapping[keys.ENTER] = enter;
        mapping[keys.ESCAPE] = escape;
        mapping[keys.SPACE] = space;
        mapping[keys.ARROW_LEFT] = previous_element;
        mapping[keys.ARROW_UP] = previous_row;
        mapping[keys.ARROW_RIGHT] = next_element;
        mapping[keys.ARROW_DOWN] = next_row;
        mapping[keys.TAB] = tabHandler;

        try {
            mapping[keycode]();
        } catch (er) {
        }

    };

    function getColumnList() {
        var columns = ['page_title'];
        if(presenter.configuration.showResults){
            columns.push('results');
        }
        if(presenter.configuration.showChecks){
            columns.push('checks');
        }
        if(presenter.configuration.showMistakes){
            columns.push('mistakes');
        }
        if(presenter.configuration.showErrors){
            columns.push('errors');
        }
        if(presenter.configuration.showPageScore){
            columns.push('score');
        }
        return columns;
    }

    function getCurrentCellTextVoiceArray() {
        var TextVoiceArray = [];
        var $cell = $(getCell(currentRow,currentColumn));

        if (currentColumn===0) {
            if(currentRow === getTableMaxHeight()-1 &amp;&amp; presenter.configuration.showTotal) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.total, ""));
            } else {
                TextVoiceArray.push(getTextVoiceObject($cell.text(), presenter.configuration.langTag));
            }
        } else {
            TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts[getColumnList()[currentColumn]],""));
            if (-1 !== getColumnList()[currentColumn].indexOf('score')) {
                TextVoiceArray.push(getTextVoiceObject($cell.text().replace('/', presenter.speechTexts.outOf, "")));
            } else {
                TextVoiceArray.push(getTextVoiceObject($cell.text(), ""));
            }
        }

        if ($cell.find(".treegrid-expander-collapsed").size()&gt;0) {
            TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.collapsed,""));
        } else if($cell.find(".treegrid-expander-expanded").size()&gt;0) {
            TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.expanded,""));
        }

        return TextVoiceArray;
    }

    presenter.readCurrentCell = function() {
        speak(getCurrentCellTextVoiceArray());
    };

    presenter.readCurrentRowAndCell = function() {
        var TextVoiceArray = [];

        if (currentColumn!==0) {
            if(currentRow === getTableMaxHeight()-1 &amp;&amp; presenter.configuration.showTotal) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.total, ""));
            } else {
                TextVoiceArray.push(getTextVoiceObject($(getCell(currentRow,0)).text(), presenter.configuration.langTag));
            }
        }

        TextVoiceArray = TextVoiceArray.concat(getCurrentCellTextVoiceArray());

        speak(TextVoiceArray);
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak(data) {
        presenter.speak(data);
    }

    presenter.speak = function(data) {
        var tts = presenter.getTextToSpeechOrNull(presentationController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    NodeRepresentation.prototype = Object.create(NodeRepresentation.prototype);
    NodeRepresentation.prototype.constructor = NodeRepresentation;

    /**
     * @param id
     * @param name
     * @param type
     * @param reportable
     * @param visited
     * @constructor
     */
    function NodeRepresentation (id, name, type, reportable, visited) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.reportable = reportable;
        this.visited = visited;
        this.children = [];

        this._isChapter = this.type === "chapter";
    }

    NodeRepresentation.prototype = Object.create(NodeRepresentation.prototype);
    NodeRepresentation.prototype.constructor = NodeRepresentation;

    NodeRepresentation.prototype.get = function (index) {
        return this.children[index];
    }

    NodeRepresentation.prototype.getId = function () {
        return this.id;
    }

    NodeRepresentation.prototype.setId = function (id) {
        this.id = id;
    }

    NodeRepresentation.prototype.getName = function () {
        return this.name;
    }

    NodeRepresentation.prototype.setName = function (name) {
        this.name = name;
    }

    NodeRepresentation.prototype.isReportable = function () {
        return this.reportable;
    }

    NodeRepresentation.prototype.isVisited = function () {
        return this.visited;
    }

    NodeRepresentation.prototype.isChapter = function () {
        return this._isChapter;
    }

    NodeRepresentation.prototype.size = function () {
        return this.children.length;
    }

    NodeRepresentation.prototype.setChildren = function (children) {
        this.children = children;
    }

    NodeRepresentation.prototype.toString = function () {
        return (
            `[id: "${this.getId()}", ` +
            `name: "${this.getName()}", ` +
            `type: "${this.type}", ` +
            `isReportable: ${this.isReportable()}, ` +
            `isVisited: ${this.isVisited()}, ` +
            `isChapter: ${this.isChapter()}, ` +
            `size: ${this.size()}]`
        );
    }

    function createDeepNodeRepresentationOfContentInformation(contentInformation, nodeRepresentation) {
        if (!nodeRepresentation) {
            nodeRepresentation = createEmptyNodeRepresentation();
        }
        let nodeRepresentationChildren = [];
        const childrenInformation = getChildrenContentInformation(contentInformation, nodeRepresentation.getId());
        childrenInformation.forEach((information) =&gt; {
            let childNodeRepresentation = createNodeRepresentationBaseOnContentInformation(information);
            if (childNodeRepresentation.isChapter()) {
                createDeepNodeRepresentationOfContentInformation(contentInformation, childNodeRepresentation);
                childNodeRepresentation.setId("chapter");
            }
            nodeRepresentationChildren.push(childNodeRepresentation);
        });
        nodeRepresentation.setChildren(nodeRepresentationChildren);
        return nodeRepresentation;
    }

    function getChildrenContentInformation(contentInformation, parentId) {
        let nodesInformation = [];
        contentInformation.forEach((information) =&gt; {
            if (information.parentId === parentId)
                nodesInformation.push(information);
        });
        return nodesInformation;
    }

    function createDeepNodeRepresentationOfNode(node, nodeRepresentation) {
        if (!nodeRepresentation) {
            nodeRepresentation = createEmptyNodeRepresentation();
        }
        let nodeRepresentationChildren = [];
        for (let i = 0; i &lt; node.size(); i++) {
            const childNode = node.get(i);
            let childNodeRepresentation = createNodeRepresentationBaseOnNode(childNode);
            if (childNodeRepresentation.isChapter()) {
                createDeepNodeRepresentationOfNode(childNode, childNodeRepresentation);
            }
            nodeRepresentationChildren.push(childNodeRepresentation);
        }
        nodeRepresentation.setChildren(nodeRepresentationChildren);
        return nodeRepresentation;
    }

    function createNodeRepresentationBaseOnNode (node) {
        const isChapter = node.type === "chapter";
        const id = isChapter ? "chapter" : node.getId();
        const name = node.getName();
        const reportable = node.isReportable ? node.isReportable() : false;
        const visited = node.isVisited ? node.isVisited() : false;
        return new NodeRepresentation(id, name, node.type, reportable, visited);
    }

    function createNodeRepresentationBaseOnContentInformation (contentInformation) {
        const id = contentInformation.id;
        const name = contentInformation.name;
        const reportable = contentInformation.isReportable === "true";
        const visited = contentInformation.isVisited === "true";
        return new NodeRepresentation(id, name, contentInformation.type, reportable, visited);
    }

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_RESULTS: 1,
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var upgradedModel = presenter.upgradeModel(model);
        presenter.printableConfiguration = presenter.validateModel(upgradedModel);
        var $view = createViewForPrintable(presenter.printableConfiguration);

        chosePrintableStateMode();
        initLessonScore();
        parsePrintableControllerLessonsScoresToValidForm();

        var $hierReport = $(findPrintableHierReport($view));
        var $table = $hierReport.find('table');
        addHeader(presenter.printableConfiguration, $table);

        var rootRepresentation = printableController.isPreview()
            ? createPreviewPrintableStructure()
            : createPrintableStructure();

        resetIndexes();
        presenter.createTree($hierReport, rootRepresentation,null);

        if (presenter.printableConfiguration.showTotal) {
            addFooter(presenter.printableConfiguration, $table);
        }
        addIndentationsInTable($table);

        cleanAfterPrintableState();
        return $view[0].outerHTML;
    };

    function addIndentationsInTable($table) {
        let contentInformation = [];
        const elementRegExp = new RegExp(CSS_CLASSES.PRINTABLE_HIER_REPORT_NODE + "-[0-9]+");
        const parentElementNodeRegExp = new RegExp(CSS_CLASSES.PRINTABLE_HIER_REPORT_PARENT + "-[0-9]+");

        let $rowsWithAddons = $table
            .find(`tr`)
            .not(`.${CSS_CLASSES.PRINTABLE_HIER_REPORT_HEADER}`)
            .not(`.${CSS_CLASSES.PRINTABLE_HIER_REPORT_FOOTER}`);

        $rowsWithAddons.each(
            function() {
                const classes = $(this).attr('class').split(' ');
                let elementRepresentation = {
                    "id": getElementIdUsingClassNameWithId(classes, elementRegExp),
                    "name": $(this),
                    "parentId": getElementIdUsingClassNameWithId(classes, parentElementNodeRegExp),
                    "type": "chapter",
                    "isReportable": null,
                    "isVisited": null,
                };
                contentInformation.push(elementRepresentation)
            }
        );

        let nodeRepresentation = createDeepNodeRepresentationOfContentInformation(contentInformation);
        addIndentationsInTableBaseOnPrintableNodesStructure(nodeRepresentation);
    }

    function getElementIdUsingClassNameWithId(elementClasses, classNameRegExp) {
        for (var i = 0; i &lt; elementClasses.length; i++) {
            var matches = classNameRegExp.exec(elementClasses[i]);
            if (matches != null) {
                var pieces = matches[0].split('-');
                return pieces[pieces.length-1];
            }
        }
        return null;
    }

    function addIndentationsInTableBaseOnPrintableNodesStructure(mainNode, indentNumber = 0) {
        var $indent = $(document.createElement('span'));
        $indent.addClass(CSS_CLASSES.PRINTABLE_HIER_REPORT_INDENT);

        for (var i = 0; i &lt; mainNode.size(); i++ ) {
            var child = mainNode.get(i);
            var $childElement = child.getName().find(`.${CSS_CLASSES.TEXT_WRAPPER}`);
            for (var indentId = 0; indentId &lt; indentNumber; indentId++) {
                $childElement.before($indent.clone());
            }
            addIndentationsInTableBaseOnPrintableNodesStructure(child, indentNumber+1);
        }
    }

    function createPreviewPrintableStructure() {
        const contentInformation = generatePreviewContentInformation();
        return createDeepNodeRepresentationOfContentInformation(contentInformation);
    }

    function generatePreviewContentInformation() {
        return [
           generatePreviewPageForContentInformation("0", "Page1", null),
           generatePreviewChapterForContentInformation("1", "Unit1", null),
           generatePreviewPageForContentInformation("2", "Page2", "1"),
           generatePreviewChapterForContentInformation("3", "Chapter1", "1"),
           generatePreviewPageForContentInformation("4", "Page3", "3"),
           generatePreviewPageForContentInformation("5", "Page4", "3"),
           generatePreviewChapterForContentInformation("6", "Chapter2", "1"),
           generatePreviewPageForContentInformation("7", "Page5", "6"),
           generatePreviewPageForContentInformation("8", "Page6", "1"),
           generatePreviewPageForContentInformation("9", "Page7", null),
           generatePreviewPageForContentInformation("10", "Page8", null),
           generatePreviewPageForContentInformation("11", "Page9", null),
           generatePreviewPageForContentInformation("12", "Page10", null),
           generatePreviewPageForContentInformation("13", "Page11", null),
       ];
    }

    function generatePreviewChapterForContentInformation(id, name, parentId) {
        return generatePreviewObjectForContentInformation(
            id, name, parentId, "chapter");
    }

    function generatePreviewPageForContentInformation(id, name, parentId) {
        return generatePreviewObjectForContentInformation(
            id, name, parentId, "page");
    }

    function generatePreviewObjectForContentInformation(id, name, parentId, type) {
        return {
            "id": id,
            "parentId": parentId,
            "name": name,
            "isReportable": "true",
            "isVisited": "false",
            "type": type
        }
    }

    function createEmptyNodeRepresentation() {
        return new NodeRepresentation(null, null, null, null, false, false);
    }

    function createPrintableStructure() {
        const contentInformation = printableController.getContentInformation();
        return createDeepNodeRepresentationOfContentInformation(contentInformation);
    }

    function createRunStructure() {
        const root = presentationController.getPresentation().getTableOfContents();
        return createDeepNodeRepresentationOfNode(root);
    }

    function createViewForPrintable(configuration) {
        var $table = $('&lt;table&gt;&lt;/table&gt;');
        $table.addClass(CSS_CLASSES.PRINTABLE_HIER_REPORT_TABLE);

        var $hier_report = $('&lt;div&gt;&lt;/div&gt;');
        $hier_report.addClass(CSS_CLASSES.PRINTABLE_HIER_REPORT);
        $hier_report.append($table);

        var $view = $("&lt;div&gt;&lt;/div&gt;");
        $view.attr("id", configuration.ID);
        $view.addClass(CSS_CLASSES.PRINTABLE);
        $view.css("max-width", configuration.width + "px");
        $view.css("min-height", configuration.height + "px");
        $view.append($hier_report);
        return $view;
    }

    function chosePrintableStateMode() {
        if (presenter.printableState) {
            presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_RESULTS;
        } else {
            presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function isInPrintableShowResultsStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_RESULTS;
    }

    function isInPrintableEmptyStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.EMPTY;
    }

    function findPrintableHierReport($view) {
        return $view.find(`.${CSS_CLASSES.PRINTABLE_HIER_REPORT}`);
    }

    function isInPrintableStateMode() {
        return presenter.printableStateMode !== null;
    }

    function cleanAfterPrintableState() {
        presenter.printableStateMode = null;
        presenter.printableLessonScore = null;
    }

    function resetIndexes() {
        absoluteIndex = 0;
        relativeIndex = 0;
        chapterIndex = 0;
        pageIndex = 0;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Hierarchical_Table_Of_Contents" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Title label" isLocalized="true" name="titleLabel" nameLabel="Hierarchical_Table_Of_Contents_property_title_label" type="string"/>
        <property displayName="Display only chapters" name="displayOnlyChapters" nameLabel="Hierarchical_Table_Of_Contents_property_display_only_chapters" type="boolean"/>
        <property displayName="Depth of expand" name="expandDepth" nameLabel="Hierarchical_Table_Of_Contents_property_expand_depth" type="string"/>
        <property displayName="Show Pages" isLocalized="true" name="showPages" nameLabel="Hierarchical_Table_Of_Contents_property_show_pages" type="{All, Reportable, Not-reportable}"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Hierarchical_Table_Of_Contents_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Hierarchical_Table_Of_Contents_property_speech_texts" type="staticlist">
            <property name="Title" nameLabel="Hierarchical_Table_Of_Contents_speech_text_title" type="staticrow">
                <property name="Title" nameLabel="Hierarchical_Table_Of_Contents_speech_text_title" type="string"/>
            </property>
            <property name="GoToPage" nameLabel="Hierarchical_Table_Of_Contents_speech_text_go_to_page" type="staticrow">
                <property name="GoToPage" nameLabel="Hierarchical_Table_Of_Contents_speech_text_go_to_page" type="string"/>
            </property>
            <property name="Chapter" nameLabel="Hierarchical_Table_Of_Contents_speech_text_chapter" type="staticrow">
                <property name="Chapter" nameLabel="Hierarchical_Table_Of_Contents_speech_text_chapter" type="string"/>
            </property>
            <property name="Expanded" nameLabel="Hierarchical_Table_Of_Contents_speech_text_expanded" type="staticrow">
                <property name="Expanded" nameLabel="Hierarchical_Table_Of_Contents_speech_text_expanded" type="string"/>
            </property>
            <property name="Collapsed" nameLabel="Hierarchical_Table_Of_Contents_speech_text_collapsed" type="staticrow">
                <property name="Collapsed" nameLabel="Hierarchical_Table_Of_Contents_speech_text_collapsed" type="string"/>
            </property>
        </property>
    </model>
<css>.hier_report {
    padding: 15px;
    border-radius: 5px;
    border: 1px solid gray;
    background-color: white;
    color: gray;
    overflow: auto;
}

.hier_report td{
    padding: 10px;
}

.hier_report-header{
    font-weight: bold;
    color: black !important;
    background-color: #bde0ff;
}
.hier_report-footer{
    font-weight: bold;
    color: black !important;
    background-color: #bde0ff;
}

.hier_report-chapter{
    background-color: Lavender;
}

.hier_report-odd{
    background-color: AliceBlue;
}

.hier_report-even{
    background-color: #f0f1ff;
}

.hier_report-progress {
    text-align:center;
    color:#0A8DBD;
    display: inline-block;
    width: 190px;
}

.hier_report-checks {
    width: 10%;
    color: green;
    text-align: center;
}

.hier_report-mistakes {
    width: 10%;
    color: #990000;
    text-align: center;
}

.hier_report-errors {
    width: 10%;
    color: red;
    text-align: center;
}

.hier_report-page-score {
    width: 10%;
    color: blue;
    text-align: center;
}

.hier_report-page-non-max-score, .hier_report-page-max-score {
    width: 10%;
    text-align: center;
}

.ui-progressbar {
    padding:2px;
    width:	130px  !important;
    height: 15px    !important;
    border-radius: 5px;
    border:2px solid #02789F;
    background-color: #3CC6CD;
    border-radius: 5px;
    float: left;
}

.hier_drag-source{
    border: 1px solid orange;
}

.hier_report .treegrid-expander {
	float: left;
}

.hier_report .treegrid-indent {
	float: left;
}

.hier_report .text-wrapper {
	overflow: hidden;
}
</css><view> &lt;div class="hier_report"&gt;
     &lt;table  style="width: 100%"&gt;
     &lt;/table&gt;
 &lt;/div&gt;

</view><preview>&lt;div class="hier_report"&gt;
    &lt;table  style="width: 100%"&gt;
    &lt;/table&gt;
&lt;/div&gt;
</preview><presenter>function AddonHierarchical_Table_Of_Contents_create() {
    var presenter = function () {};
    var presentationController;
    var pageIndex = 0;

    presenter.isVisible = null;
    presenter.keyboardControllerObject = null;

    presenter.ERROR_MESSAGES = {
        EXPAND_DEPTH_NOT_NUMERIC: "Depth of expand is not proper",
    };

    presenter.CSS_CLASSES = {
        TABLE: "table",
        TITLE: "hier_report-header",
        FOOTER: "hier_report-footer",
        TABLE_CONTAINER: "hier_report",
        CHAPTER: "hier_report-chapter",
        CHAPTER_EXPANDER: "treegrid-expander",
        CHAPTER_EXPANDER_EXPANDED: "treegrid-expander-expanded",
        CHAPTER_EXPANDER_COLLAPSED: "treegrid-expander-collapsed",
        CURRENT_PAGE: "current-page",
    };

    presenter.DEFAULT_TTS_PHRASES = {
        Title: "Title",
        GoToPage: "Go to page",
        Chapter: "Chapter",
        Collapsed: "Collapsed",
        Expanded: "Expanded",
    };

    presenter.isFirstEnter = true;

    function returnErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    function returnCorrectObject(v) { return { isValid: true, value: v }; }

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                if (substitutions.hasOwnProperty(key)) {
                    messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
                }
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }
        presenter.$view.html(errorContainer);
    };

    presenter.setPlayerController = function (controller) {
        presentationController = controller;
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    function addHeader() {
        var headerHTML = "&lt;td&gt; " + presenter.configuration.labels.title + "&lt;/td&gt;";
        $("&lt;tr&gt;&lt;/tr&gt;").prependTo(getJqueryTable()).addClass("hier_report-header").html(headerHTML);
    }

    function addFooter() {
        var row = document.createElement('tr');
        $(row).appendTo(getJqueryTable());
        $(row).addClass("hier_report-footer");
    }

    function createRow(index, parentIndex, isChapter) {
        var row = document.createElement('tr');

        $(row).appendTo(getJqueryTable());
        $(row).addClass("treegrid-" + index);

        if (parentIndex != null) {
            $(row).addClass("treegrid-parent-" + parentIndex);
        }

        if (isChapter) {
            $(row).addClass("hier_report-chapter");
        } else {
            $(row).addClass(index % 2 &gt; 0 ? "hier_report-odd" : "hier_report-even");
        }

        return row;
    }

    function createRowCell(text, isChapter, pageId) {
        const rowCell = document.createElement('td');

        const textWrapper = document.createElement('div');
        textWrapper.classList.add('text-wrapper');
        if (isChapter) {
            textWrapper.textContent = text;
        } else {
            const link = document.createElement('a');
            link.textContent = text;
            link.setAttribute('href', '#');
            link.setAttribute('data-page-id', pageId);

            textWrapper.appendChild(link);
            if (!presenter.isPreview){
                const presentation = presentationController.getPresentation();
                const currentPageId = presentation.getPage(presentationController.getCurrentPageIndex()).getId();

                if (currentPageId === pageId) {
                    link.classList.add(presenter.CSS_CLASSES.CURRENT_PAGE);
                }
            }
        }
        rowCell.append(textWrapper);

        return rowCell;
    }

    function addRow(name, index, parentIndex, isChapter, pageId) {
        const row = createRow(index, parentIndex, isChapter);
        const cell = createRowCell(name, isChapter, pageId);
        row.append(cell);
    }

    function resetScore() {
        return {
            score: 0,
            countedScore: 0,
            maxScore: 0,
            countedMaxScore: 0,
            errorCount: 0,
            checkCount: 0,
            mistakeCount: 0,
            count: 0
        };
    }

    presenter.createPreviewTree = function() {
        var pagesMockup = [
            {name : "Page1", parent : null},
            {name : "Unit1", parent : null},
            {name : "Chapter2", parent : 1},
            {name : "Page5", parent : 6},
            {name : "Page6", parent : 1},
            {name : "Page7", parent : null},
            {name : "Page8", parent : null},
            {name : "Page9", parent : null},
            {name : "Page10", parent : null},
            {name : "Page11", parent : null},
            {name : "Unit2", parent : null},
            {name : "Page12", parent : 10},
            {name : "Page13", parent : 10}
        ];

        var chapterScore = resetScore();
        for (var i = 0; i &lt; pagesMockup.length; i++) {
            if(pagesMockup[i].name == "Unit1" || pagesMockup[i].name == "Unit2"){
                addRow(pagesMockup[i].name, i, pagesMockup[i].parent, true, "some_id");
            }else{
                addRow(pagesMockup[i].name, i, pagesMockup[i].parent, false, "some_id");
            }
        }
        return chapterScore;
    };

    presenter.createTree = function (root, parentIndex, pageCount) {
        var chapterIndex = 0,
            chapterScore = resetScore(),
            pageScore = resetScore(),
            isEmpty = true,
            values = {},
            isEnabled = true;

        for (var i = 0; i &lt; pageCount; i++) {
            var isChapter = (root.get(i).type == "chapter");

            if (!isChapter &amp;&amp; !root.get(i).isReportable() &amp;&amp; presenter.configuration.showPages == "Reportable") continue;
            if (!isChapter &amp;&amp; root.get(i).isReportable() &amp;&amp; presenter.configuration.showPages == "Not-reportable") continue;
            if (!isChapter &amp;&amp; root.get(i).isReportable()) {
                isEmpty = false;
            }
            var pageId = "chapter";
            if (!isChapter) {
                pageId = root.get(i).getId();
            }
            addRow(root.get(i).getName(), pageIndex, parentIndex, isChapter, pageId);
            pageScore = presentationController.getScore().getPageScoreById(pageId);
            pageScore.count = 1;
            pageIndex++;
            if (isChapter) {
                chapterIndex = pageIndex - 1;
                values = presenter.createTree(root.get(i), chapterIndex, root.get(i).size());
                pageScore =  values.pagesScore;
            }
        }

        return { pagesScore: chapterScore, isEmpty: isEmpty };
    };

    function handleMouseClickActions() {
        var commander = presentationController.getCommands(),
            $report = presenter.$view.find('.hier_report tr');

        $report.find('td a').each(function () {
            $(this).click(function (event) {
                event.preventDefault();
                event.stopPropagation();
                commander.gotoPageId($(this).attr('data-page-id'));
            });
        });

        $report.find(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER}`).each(function () {
            $(this).click(function (event) {
                event.preventDefault();
                event.stopPropagation();
            });
        });
    }

    function expandTree(level) {
        presenter.$view.find('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function () {
            if ($(this).treegrid('getDepth') &lt; level) {
                $(this).treegrid('expand'); 
            }
        });
    }

    function saveTreeState() {
        var state = [];
        $('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function () {
            state.push($(this).treegrid('isExpanded'))
        });
        return state;
    }

    function restoreTreeState(state) {
        $('.hier_report table').find('tr').not('.hier_report-header').not('.hier_report-footer').each(function () {
            $(this).treegrid(state[$(this).treegrid('getNodeId')] ? 'expand' : 'collapse');
        });
    }

    presenter.getState = function () {
        return JSON.stringify({
            'treeState': saveTreeState(),
            'isVisible': presenter.isVisible,
        });
    };

    presenter.setState = function (stateString) {
        var state = JSON.parse(stateString);

        restoreTreeState(state.treeState);

        presenter.setVisibility(state.isVisible);
    };

    presenter.validateModel = function (model) {
        var expandDepth = returnCorrectObject(0);
        if (model['expandDepth'].length &gt; 0) {
            expandDepth = ModelValidationUtils.validateInteger(model['expandDepth']);
            if (!expandDepth.isValid) {
                return returnErrorObject('EXPAND_DEPTH_NOT_NUMERIC');
            }
        }
        return {
            ID: model.ID,
            isValid: true,
            width: parseInt(model["Width"], 10),
            height: parseInt(model["Height"], 10),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            labels: {
                title: model['titleLabel']
            },
            displayOnlyChapters: ModelValidationUtils.validateBoolean(model.displayOnlyChapters),
            expandDepth: expandDepth.value,
            showPages: model.showPages,
            langTag: model.langAttribute,
        };
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.isVisible = isVisible;
    };

    presenter.show = function () {
        if (!presenter.isVisible) {
            presenter.setVisibility(true);
        }
    };

    presenter.hide = function () {
        if (presenter.isVisible) {
            presenter.setVisibility(false);
        }
    };

    presenter.reset = function () {
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.initialize = function (view, model, isPreview) {
        const upgradedModel = presenter.upgradeModel(model);

        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[presenter.configuration.errorCode]);
            return;
        }

        presenter.$view = $(view);
        presenter.isPreview = isPreview;
        presenter.lessonScore = {
            pageCount: 0,
            checks: 0,
            errors: 0,
            mistakes: 0,
            score: 0,
            maxScore: 0,
            scaledScore: 0
        };

        $('.hier_report').attr("style", "height: " + presenter.configuration.height + "px");
        presenter.treeID = presenter.configuration.ID + (isPreview ? "Preview" : "");
        presenter.$view.find("div").first().attr('id', presenter.treeID);

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);


        addHeader();
        if (isPreview) {
            presenter.createPreviewTree();
        } else {
            var presentation = presentationController.getPresentation();
            presenter.createTree(presentation.getTableOfContents(), null, presentation.getTableOfContents().size());
            checkIfChapterHasChildren(false);
            if(presenter.configuration.displayOnlyChapters){
                displayOnlyChapters();
            }
        }

        if (presenter.configuration.showTotal) {
            addFooter();
        }

        getJqueryTable().not('.hier_report-header').not('.hier_report-footer').treegrid({
            'initialState': 'collapsed',
            'expanderTemplate': '&lt;div class="treegrid-expander"&gt;&lt;/div&gt;'
        });

        expandTree(presenter.configuration.expandDepth);
        if (!isPreview) {
            handleMouseClickActions();
        }

        if(presenter.configuration.displayOnlyChapters){
            presenter.$view.find("tr").each(function () {
                if($(this).hasClass("hier_report-even") || $(this).hasClass("hier_report-odd")){
                    $(this).remove();
                }
            });
        }

        checkIfChapterHasChildren(true);

        presenter.setSpeechTexts(upgradedModel["speechTexts"]);
        presenter.buildKeyboardController();
    };

    presenter.upgradeModel = function (model) {
        const upgradedLangTagModel = presenter.upgradeLangTag(model);
        const upgradedSpeechTextsModel = presenter.upgradeSpeechTexts(upgradedLangTagModel);
        const upgradedDepthOfExpandModel = presenter.upgradeDepthOfExpand(upgradedSpeechTextsModel);

        return upgradedDepthOfExpandModel;
    };

    presenter.upgradeLangTag = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) upgradedModel["speechTexts"] = {};

        const modelSpeechTexts = upgradedModel["speechTexts"];

        if (!modelSpeechTexts["Title"]) modelSpeechTexts["Title"] = {Title: ""};
        if (!modelSpeechTexts["GoToPage"]) modelSpeechTexts["GoToPage"] = {GoToPage: ""};
        if (!modelSpeechTexts["Chapter"]) modelSpeechTexts["Chapter"] = {Chapter: ""};
        if (!modelSpeechTexts["Expanded"]) modelSpeechTexts["Expanded"] = {Expanded: ""};
        if (!modelSpeechTexts["Collapsed"]) modelSpeechTexts["Collapsed"] = {Collapsed: ""};

        return upgradedModel;
    };

    presenter.upgradeDepthOfExpand = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["expandDepth"] === undefined) {
            upgradedModel["expandDepth"] = '';
        }

        return upgradedModel;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            ...presenter.DEFAULT_TTS_PHRASES
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Title: TTSUtils.getSpeechTextProperty(
                speechTexts.Title.Title,
                presenter.speechTexts.Title),
            GoToPage: TTSUtils.getSpeechTextProperty(
                speechTexts.GoToPage.GoToPage,
                presenter.speechTexts.GoToPage),
            Chapter: TTSUtils.getSpeechTextProperty(
                speechTexts.Chapter.Chapter,
                presenter.speechTexts.Chapter),
            Expanded: TTSUtils.getSpeechTextProperty(
                speechTexts.Expanded.Expanded,
                presenter.speechTexts.Expanded),
            Collapsed: TTSUtils.getSpeechTextProperty(
                speechTexts.Collapsed.Collapsed,
                presenter.speechTexts.Collapsed)
        };
    };

    function displayOnlyChapters() {
        presenter.$view.find(".hier_report-chapter").each(function () {
            var element = $(this);
            element.find(".text-wrapper").wrap('&lt;a href=""&gt;&lt;/a&gt;');
            if(element.next('tr[class*=treegrid-parent]').length &gt; 0){
                var dataPageId = element.next().find("a").attr("data-page-id");
                element.find("a").attr("data-page-id", dataPageId);
            }
        });
    }

    function checkIfChapterHasChildren (isDisplayOnlyChapters) {
        presenter.$view.find(".hier_report-chapter").each(function () {
            if($(this).next('tr[class*=treegrid-parent]').length == 0){
                if(isDisplayOnlyChapters){
                    $(this)
                        .find(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER}`)
                        .removeClass(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER_COLLAPSED}`)
                        .removeClass(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER_EXPANDED}`);
                }else{
                    $(this).remove();
                }
            }
        });
    }

    function getJqueryTable() {
        return presenter.$view.find("#" + presenter.treeID).find('table');
    }

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    function HTocKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    HTocKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    HTocKeyboardController.prototype.constructor = HTocKeyboardController;

    presenter.buildKeyboardController = function () {
        const keyNavElements = presenter.getElementsForKeyboardNavigation();

        presenter.keyboardControllerObject = new HTocKeyboardController(keyNavElements, 1);
        presenter.keyboardControllerObject.selectEnabled(true);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        const tableRows = presenter.$view.find("tbody")
            .find("tr")
            .filter((i, element) =&gt; element.className.includes("treegrid"))
            .map((i, element) =&gt; $(element));

        return tableRows.toArray();
    };

    presenter.getElementsForTTS = function() {
        const elements = presenter.getElementsForKeyboardNavigation();
        elements.unshift(presenter.getTitleElementForKeyboardNavigation());

        return elements;
    };

    presenter.getTitleElementForKeyboardNavigation = function() {
        return $(presenter.$view.find(`.${presenter.CSS_CLASSES.TITLE}`)[0]);
    };

    // Right Arrow or TAB
    HTocKeyboardController.prototype.nextElement = function (event) {
        if (event) event.preventDefault();
        this.moveToNextKeyNavElement();
    };

    // Left Arrow or SHIFT+TAB
    HTocKeyboardController.prototype.previousElement = function (event) {
        if (event) event.preventDefault();
        this.moveToPreviousKeyNavElement();
    };

    // Down Arrow
    HTocKeyboardController.prototype.nextRow = function (event) {
        if (event) event.preventDefault();
        this.moveToNextKeyNavElement();
    };

    // Up Arrow
    HTocKeyboardController.prototype.previousRow = function (event) {
        if (event) event.preventDefault();
        this.moveToPreviousKeyNavElement();
    };

    HTocKeyboardController.prototype.moveToNextKeyNavElement = function () {
        const nextIndex = this.getNextSelectableElementIndexOrNull();
        if (nextIndex === null) return;

        this.markCurrentElement(nextIndex);

        centerElement(this.keyboardNavigationCurrentElement);
    };

    HTocKeyboardController.prototype.moveToPreviousKeyNavElement = function () {
        const previousIndex = this.getPreviousSelectableElementIndexOrNull()
        if (previousIndex === null) return;

        this.markCurrentElement(previousIndex);

        centerElement(this.keyboardNavigationCurrentElement);
    };

    HTocKeyboardController.prototype.getNextSelectableElementIndexOrNull = function () {
        const elements = this.keyboardNavigationElements;
        const nextElementIndex = this.keyboardNavigationCurrentElementIndex + 1;

        for (let i = nextElementIndex; i &lt; elements.length; i++) {
            if (presenter.isParentTableRowVisible(elements[i])) {
                return i;
            }
        }
        return null;
    };

    HTocKeyboardController.prototype.getPreviousSelectableElementIndexOrNull = function () {
        const elements = this.keyboardNavigationElements;
        const nextElementIndex = this.keyboardNavigationCurrentElementIndex - 1;

        for (let i = nextElementIndex; i &gt;= 0; i--) {
            if (presenter.isParentTableRowVisible(elements[i])) {
                return i;
            }
        }
        return null;
    };

    presenter.isParentTableRowVisible = function (element) {
        return $(element).closest("tr").style('display') !== "none";
    };

    HTocKeyboardController.prototype.enter = function (event) {
        if (presenter.isFirstEnter) {
            if (presenter.isTTS()) {
                this.setElements.call(this, presenter.getElementsForTTS());
            }
            presenter.isFirstEnter = false;
        }
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    HTocKeyboardController.prototype.select = function (event) {
        if (event) event.preventDefault();
        if (!this.isSelectEnabled) {
            return;
        }

        this.selectAction();
        if (this.isCurrentElementSelectable()) {
            const $element = $(this.getCurrentElement());
            const expanderTTSPostFix = isChapterExpanderExpanded($element)
                ? presenter.speechTexts.Expanded
                : presenter.speechTexts.Collapsed;

            presenter.speak(expanderTTSPostFix);
        }
    };

    HTocKeyboardController.prototype.selectAction = function () {
        const $currentElement = $(this.getCurrentElement());

        const clickableElement = $currentElement.hasClass(presenter.CSS_CLASSES.CHAPTER)
            ? $currentElement.find(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER}`)
            : $currentElement.find("a");

        this.getTarget(clickableElement, true).click();
    };


    HTocKeyboardController.prototype.markCurrentElement = function (nextIndex) {
        KeyboardController.prototype.markCurrentElement.call(this, nextIndex);
        if(this.isCurrentElementDisplayed()) {
            this.readCurrentElement();
        }
    };

    HTocKeyboardController.prototype.readCurrentElement = function () {
        const $element = this.getTarget(this.keyboardNavigationCurrentElement, false);

        if ($element.hasClass(presenter.CSS_CLASSES.TITLE)) {
            presenter.speak(getTitleTTS($element));
        } else if ($element.hasClass(presenter.CSS_CLASSES.CHAPTER)) {
            presenter.speak(getChapterTTS($element));
        } else {
            presenter.speak(getHyperLinkTTS($element));
        }
    };

    HTocKeyboardController.prototype.isCurrentElementDisplayed = function () {
        const currentElement = this.getCurrentElement();
        return presenter.isParentTableRowVisible(currentElement);
    };

    HTocKeyboardController.prototype.isCurrentElementSelectable = function () {
        const currentElement = this.getCurrentElement();
        return !($(currentElement).hasClass("hier_report-header"));
    };

    HTocKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    HTocKeyboardController.prototype.exitWCAGMode = function () {
        presenter.isFirstEnter = true;
        this.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    function getTitleTTS($element) {
        const textVoiceObject = [];

        const titlePrefix = presenter.speechTexts.Title;
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, titlePrefix);

        const titleText = $element[0].innerText;
        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, titleText);

        return textVoiceObject;
    }

    function getChapterTTS ($element) {
        const textVoiceObject = [];

        const chapterPrefix = presenter.speechTexts.Chapter;
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, chapterPrefix);

        const chapterName = $element.find(".text-wrapper").text();
        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, chapterName);

        const expanderStatus = isChapterExpanderExpanded($element)
            ? presenter.speechTexts.Expanded
            : presenter.speechTexts.Collapsed;

        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, expanderStatus);

        return textVoiceObject;
    }

    function getHyperLinkTTS($element) {
        const textVoiceObject = [];

        const goToPagePrefix = presenter.speechTexts.GoToPage;
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, goToPagePrefix);

        const pageName = $element[0].innerText;
        pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, pageName);

        return textVoiceObject;
    }

    function pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, message) {
        textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    function pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, message) {
        textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
    }

    function centerElement(element){
        scrollVertically(element);
    }

    function scrollVertically(element) {
        let pos = $(element).position().top;
        let currentScroll = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_CONTAINER}`).scrollTop();
        let divHeight =  presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_CONTAINER}`).height();

        pos=(pos+currentScroll)-(divHeight/2);

        presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_CONTAINER}`).scrollTop(pos);
    }

    function isChapterExpanderExpanded ($element) {
        return $element
            .find(`.${presenter.CSS_CLASSES.CHAPTER_EXPANDER}`)
            .hasClass(presenter.CSS_CLASSES.CHAPTER_EXPANDER_EXPANDED);
    }

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.speak = function(data) {
        const tts = presenter.getTextToSpeechOrNull(presentationController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.isTTS = function () {
        return presenter.getTextToSpeechOrNull(presentationController) &amp;&amp; presenter.isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function HToc_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="IWB_Toolbar" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Floating Images" name="floatingImages" nameLabel="IWB_Toolbar_property_floating_images" type="list">
            <property name="Image" nameLabel="IWB_Toolbar_property_image" type="image"/>
        </property>
        <property displayName="Width When Opened" name="widthWhenOpened" nameLabel="IWB_Toolbar_property_width_when_opened" type="string"/>
        <property name="Fixed Position" nameLabel="IWB_Toolbar_property_fixed_position" type="boolean"/>

        <property displayName="Show for pen" name="forPen" nameLabel="IWB_Toolbar_property_for_pen" type="{---, Color panel, Thickness panel}"/>
        <property displayName="Show for marker" name="forMarker" nameLabel="IWB_Toolbar_property_for_marker" type="{---, Color panel, Thickness panel}"/>
        <property displayName="Keep state and position" name="keepStateAndPosition" nameLabel="IWB_Toolbar_property_keep_state_and_position" type="boolean"/>
        <property name="Closed panel drawing" nameLabel="IWB_Toolbar_property_closed_panel_drawing" type="boolean"/>
        <property name="hasCustomButton" nameLabel="IWB_Toolbar_property_has_custom_button" type="boolean"/>
        <property isDefault="true" name="onCustomButtonSelected" nameLabel="IWB_Toolbar_property_on_custom_button_selected" type="event"/>
        <property name="onCustomButtonDeselected" nameLabel="IWB_Toolbar_property_on_custom_button_deselected" type="event"/>
        <property name="enableUndoRedo" nameLabel="IWB_Toolbar_property_enable_undo_redo" type="boolean"/>
        <property name="disableModuleZoom" nameLabel="IWB_Toolbar_property_disable_module_zoom" type="boolean"/>
        <property name="defaultZoom" nameLabel="IWB_Toolbar_property_default_zoom" type="string"/>
        <property displayName="Expansion direction" name="expansionDirection" nameLabel="IWB_Toolbar_expansion_direction" type="{right, left, up, down}"/>
        <property displayName="Vertical popups direction" name="verticalPopupDirection" nameLabel="IWB_Toolbar_vertical_popups_direction" type="{right, left}"/>
        <property displayName="Horizontal popups direction" name="horizontalPopupDirection" nameLabel="IWB_Toolbar_horizontal_popups_direction" type="{down, up}"/>
    </model>
<css>.iwb-toolbar-panel {
    padding: 5px 25px;
    cursor: move;
    border-radius: 3px;
    min-width: 30px;
    min-height: 30px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.6);
    background-color: rgba(255, 255, 255, 0.8);
    background-image: url('resources/wb_panel_move_icon.svg'), url('resources/wb_panel_move_icon.svg');
    background-position: left center, right center;
    background-repeat: no-repeat;
}

.iwb-toolbar-panel.right {
    width: 30px;
}

.iwb-toolbar-panel.left {
    display: flex;
    flex-direction: row-reverse;
    flex-wrap: wrap;
    width: 30px;
}

.iwb-toolbar-panel.up {
    display: flex;
    flex-direction: column-reverse;
    flex-wrap: wrap;
    height: 30px;
}

.iwb-toolbar-panel.down {
    display: flex;
    flex-direction: column;
    flex-wrap: wrap;
    height: 30px;
}

.iwb-toolbar-panel.running {
    z-index: 1001;
    position: fixed;
}

.iwb-toolbar-panel .button-separator {
    width: 1px;
    float: left;
    height: 30px;
    background-color: rgba(150, 150, 150, 0.6);
    margin-left: 5px;
}

.iwb-toolbar-panel.up .button-separator,
.iwb-toolbar-panel.down .button-separator {
    width: 30px;
    float: inherit;
    height: 1px;
    margin-top: 5px;
    margin-left: 0px;
}

.iwb-toolbar-panel .button.clicked {
    background-color: #7A7A7A;
}

.iwb-toolbar-panel .button.clicked-lighter, .iwb-toolbar-panel .button-drawing-details.clicked-lighter, .iwb-toolbar-panel .button-floating-image.clicked-lighter {
    background-color: #CACACA;
}

.iwb-toolbar-panel .button, .iwb-toolbar-panel .button-drawing-details, .iwb-toolbar-panel .button-floating-image {
    width: 30px;
    height: 30px;
    float: left;
    text-align: center;
    border-radius: 3px;
    font-size: 16px;
    line-height: 30px;
    margin-left: 5px;
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
}

.iwb-toolbar-panel.up .button,
.iwb-toolbar-panel.up .button-drawing-details,
.iwb-toolbar-panel.up .button-floating-image,
.iwb-toolbar-panel.down .button,
.iwb-toolbar-panel.down .button-drawing-details,
.iwb-toolbar-panel.down .button-floating-image {
    float: inherit;
    margin-top: 5px;
    margin-left: 0px;
}

.iwb-toolbar-panel.left .button.first,
.iwb-toolbar-panel.right .button.first {
    margin-left: 0px;
}

.iwb-toolbar-panel.up .button.first,
.iwb-toolbar-panel.down .button.first{
    margin-top: 0px;
}

.iwb-toolbar-panel .button.hidden,
.iwb-toolbar-panel .button-separator.hidden {
    display: none;
}

.iwb-toolbar-panel .button.hovered {
    cursor: pointer;
}

.iwb-toolbar-mask {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
    cursor: crosshair;
}

.iwb-toolbar-mask.pen-mask {
    z-index: 99;
}

.iwb_tmp_canvas {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0px;
    left: 0px;
    cursor: crosshair;
}

.iwb-toolbar-mask.marker-mask {
    z-index: 100;
}

.iwb-toolbar-mask canvas {
    width: 100%;
    height: 100%;
}

.iwb-toolbar-selection {
    border: 1px dotted #0000ff;
    position: absolute;
}

.iwb-toolbar-panel.right .bottom-panel .container {
    float: right;
    margin-top: 5px;
}

.iwb-toolbar-panel.left .bottom-panel .container {
    display: flex;
    flex-direction: row-reverse;
    float: right;
    margin-top: 5px;
}

.iwb-toolbar-panel.up .bottom-panel .container {
    display: flex;
    flex-direction: column-reverse;
    float: right;
    margin-left: 5px;
}

.iwb-toolbar-panel.down .bottom-panel .container {
    display: flex;
    flex-direction: column;
    float: right;
    margin-left: 5px;
}

.iwb-toolbar-panel.right .bottom-panel-floating-image {
    margin-right: 77px;
}

.iwb-toolbar-panel.left .bottom-panel-floating-image {
    margin-left: 77px;
}

.iwb-toolbar-panel.up .bottom-panel-floating-image {
    margin-top: 77px;
}

.iwb-toolbar-panel.down .bottom-panel-floating-image {
    margin-bottom: 77px;
}

.iwb-toolbar-panel .bottom-panel {
    display: none;
}

.iwb-toolbar-panel.left .bottom-panel {
    margin-right: auto;
}

.iwb-toolbar-panel.up .bottom-panel {
    margin-bottom: auto;
}

.iwb-toolbar-panel.down .bottom-panel {
    margin-top: auto;
}

.iwb-toolbar-panel .button.close, .iwb-toolbar-panel .button.open {
    font-size: 10px;
    line-height: 30px;
}

.iwb-toolbar-panel .button.open {
    display: block;
    background-image: url('resources/wb-panel_open.svg');
}

.iwb-toolbar-panel .button.close {
    background-image: url('resources/wb-panel_close.svg');
}

.iwb-toolbar-panel .button.zoom {
    background-image: url('resources/wb_panel_zoom_icon.svg');
}

.iwb-toolbar-panel .button.zoom.clicked {
    background-image: url('resources/wb_panel_zoom_icon-active.svg');
}

.iwb-toolbar-panel .button.pen {
    background-image: url('resources/wb_panel_pen_icon.svg');
}

.iwb-toolbar-panel .button.pen.clicked {
    background-image: url('resources/wb_panel_pen_icon-active.svg');
}

.iwb-toolbar-panel .button.marker {
    background-image: url('resources/wb_panel_marker_icon.svg');
}

.iwb-toolbar-panel .button.marker.clicked {
    background-image: url('resources/wb_panel_marker_icon-active.svg');
}

.iwb-toolbar-panel .button.stand-area {
    background-image: url('resources/wb_panel_blind_icon.svg');
}

.iwb-toolbar-panel .button.stand-area.clicked {
    background-image: url('resources/wb_panel_blind_icon-active.svg');
}

.iwb-toolbar-panel .button.hide-area {
    background-image: url('resources/wb_panel_focus_icon.svg');
}

.iwb-toolbar-panel .button.hide-area.clicked {
    background-image: url('resources/wb_panel_focus_icon-active.svg');
}

.iwb-toolbar-panel .button.eraser {
    background-image: url('resources/wb_panel_erase_icon.svg');
}

.iwb-toolbar-panel .button.eraser.clicked {
    background-image: url('resources/wb_panel_erase_icon-active.svg');
}

.iwb-toolbar-panel .button.reset {
    background-image: url('resources/wb_panel_reset_icon.svg');
}

.iwb-toolbar-panel .button.reset.clicked {
    background-image: url('resources/wb_panel_reset_icon-active.svg');
}

.iwb-toolbar-panel .button.reset-one {
    background-image: url('resources/undo-one.svg');
}

.iwb-toolbar-panel .button.reset-one.clicked {
    background-image: url('resources/undo-one.svg');
}

.iwb-toolbar-panel .button.redo-one {
    background-image: url('resources/redo-one.svg');
}

.iwb-toolbar-panel .button.redo-one.clicked {
    background-image: url('resources/redo-one.svg');
}

.iwb-toolbar-panel .button.default {
    background-image: url('resources/wb_panel_cursor_icon.svg');
}

.iwb-toolbar-panel .button.default.clicked {
    background-image: url('resources/wb_panel_cursor_icon-active.svg');
}

.iwb-toolbar-panel .button.clock {
    background-image: url('resources/wb_panel_clock_icon.svg');
}

.iwb-toolbar-panel .button.clock.clicked {
    background-image: url('resources/wb_panel_clock_icon-active.svg');
}

.iwb-toolbar-panel .button.stopwatch {
    background-image: url('resources/wb_panel_stopwatch_icon.svg');
}

.iwb-toolbar-panel .button.stopwatch.clicked {
    background-image: url('resources/wb_panel_stopwatch_icon-active.svg');
}

.iwb-toolbar-panel .button.note {
    background-image: url('resources/wb_panel_note_icon.svg');
}

.iwb-toolbar-panel .button.note.clicked {
    background-image: url('resources/wb_panel_note_icon-active.svg');
}

.iwb-toolbar-panel .button.custom-script {
    background-image: url('resources/wb_panel_custom_script_icon.svg');
}

.iwb-toolbar-panel .button.custom-script.clicked {
    background-image: url('resources/wb_panel_custom_script_icon-active.svg');
}

.iwb-toolbar-panel .button.floating-image {
    background-image: url('resources/wb_panel_rullers_icon.svg');
}

.iwb-toolbar-panel .button.floating-image.clicked {
    background-image: url('resources/wb_panel_rullers_icon-active.svg');
}

.iwb-toolbar-panel .button.color {
    background-image: url('resources/wb_panel_color_black_icon.svg');
}

.iwb-toolbar-panel .button.thickness.clicked {
    background-image: url('resources/wb_panel_size1_icon-active.svg');
}

.iwb-toolbar-panel .button.thickness {
    background-image: url('resources/wb_panel_size1_icon.svg');
}

.iwb-toolbar-panel .color-black {
    background-image: url('resources/wb_panel_color_black_icon.svg');
}

.iwb-toolbar-panel .color-blue {
    background-image: url('resources/wb_panel_color_blue_icon.svg');
}

.iwb-toolbar-panel .color-green {
    background-image: url('resources/wb_panel_color_green_icon.svg');
}

.iwb-toolbar-panel .color-orange {
    background-image: url('resources/wb_panel_color_orange_icon.svg');
}

.iwb-toolbar-panel .color-red {
    background-image: url('resources/wb_panel_color_red_icon.svg');
}

.iwb-toolbar-panel .color-violet {
    background-image: url('resources/wb_panel_color_violet_icon.svg');
}

.iwb-toolbar-panel .color-white {
    background-image: url('resources/wb_panel_color_white_icon.svg');
}

.iwb-toolbar-panel .color-yellow {
    background-image: url('resources/wb_panel_color_yellow_icon.svg');
}

.iwb-toolbar-panel .thickness-1 {
    background-image: url('resources/wb_panel_size1_icon.svg');
}

.iwb-toolbar-panel .thickness-1.clicked {
    background-image: url('resources/wb_panel_size1_icon-active.svg');
}

.iwb-toolbar-panel .thickness-2 {
    background-image: url('resources/wb_panel_size2_icon.svg');
}

.iwb-toolbar-panel .thickness-2.clicked {
    background-image: url('resources/wb_panel_size2_icon-active.svg');
}

.iwb-toolbar-panel .thickness-3 {
    background-image: url('resources/wb_panel_size3_icon.svg');
}

.iwb-toolbar-panel .thickness-3.clicked {
    background-image: url('resources/wb_panel_size3_icon-active.svg');
}

.iwb-toolbar-panel .thickness-4 {
    background-image: url('resources/wb_panel_size4_icon.svg');
}

.iwb-toolbar-panel .thickness-4.clicked {
    background-image: url('resources/wb_panel_size4_icon-active.svg');
}

.button-floating-image.button-floating-image-1 {
    background-image: url('resources/wb_panel_ruller1_icon.svg');
}

.button-floating-image.button-floating-image-2 {
    background-image: url('resources/wb_panel_ruller2_icon.svg');
}

.button-floating-image.button-floating-image-3 {
    background-image: url('resources/wb_panel_ruller3_icon.svg');
}

.iwb-toolbar-stopwatch .stopwatch-buttons #start{
    width: 20px;
    height: 20px;
    float: left;
    margin-right: 10px;
    background-image: url('resources/start.png');
    background-size: 20px;
    background-position: 6px center;
    background-repeat: no-repeat;
    cursor: pointer;
    border-radius: 20px;
    padding: 5px;
}

.iwb-toolbar-stopwatch .stopwatch-buttons #stop{
    width: 20px;
    height: 20px;
    float: left;
    margin-right: 10px;
    background-image: url('resources/stop.png');
    background-size: 20px;
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
    border-radius: 20px;
    padding: 5px;
}

.iwb-toolbar-stopwatch .stopwatch-buttons #clear{
    width: 20px;
    height: 20px;
    float: left;
    background-image: url('resources/reset.png');
    border-radius: 5px;
    background-size: 20px;
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
    border-radius: 20px;
    padding: 5px;
}

.iwb-toolbar-stopwatch .stopwatch-buttons .button-clicked{
    background-color: #51BB59;
}

.iwb-toolbar-stopwatch {
    top: 0px;
    width: 180px;
    height: auto;
    position: absolute;
    background: #A6E9B9;
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    overflow: hidden;
}

.iwb-toolbar-stopwatch .stopwatch-header {
    height: 20px;
    text-align: center;
    background: url('resources/wb_panel_stopwatch_icon.svg') no-repeat;
    background-size: 15px;
    background-position: 2px;
    cursor: move;
    background-color: rgba(255, 255, 255, 0.5);
}

.iwb-toolbar-stopwatch .stopwatch-header .stopwatch-close{
    width: 10%;
    right: 5px;
    float: right;
    position: relative;
    cursor: pointer;
    text-align: right;
}

.iwb-toolbar-stopwatch .stopwatch-header h4{
    float: left;
    margin-left: 50px;
    color: #008000;
}

.iwb-toolbar-stopwatch .stopwatch-buttons {
    padding: 5px;
    margin-left: 30px;
    font-size: 0.8em;
    word-wrap: break-word;
    overflow-wrap: break-word;
    text-align: center;
    height: 30px;
}

.iwb-toolbar-stopwatch .stopwatch-buttons .stopwatch-button{
    padding: 5px;
    font-size: 1em;
    word-wrap: break-word;
    overflow-wrap: break-word;
    text-align: center;
}

.iwb-toolbar-clock {
    top: 0px;
    width: 100px;
    height: auto;
    position: absolute;
    background-color: #9ECDFF;
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    overflow: hidden;
}

.iwb-toolbar-clock .clock-header {
    height: 20px;
    background-image: url('resources/wb_panel_clock_icon.svg');
    background-repeat: no-repeat;
    background-size: 15px;
    background-position: 2px;
    cursor: move;
    background-color: rgba(255, 255, 255, 0.5);
}
.iwb-toolbar-clock .clock-header .clock-close{
    width: 10%;
    right: 5px;
    float: right;
    position: relative;
    cursor: pointer;
    text-align: right;
}

.iwb-toolbar-clock .clock-body {
    padding: 5px;
    font-size: 16px;
    word-wrap: break-word;
    overflow-wrap: break-word;
    white-space: pre;
    text-align: center;
    color: #006ED3;
}

.iwb-toolbar-note {
    top: 0px;
    width: 185px;
    min-height: 120px;
    max-height: 350px;
    position: absolute;
    background-color: rgba(255, 255, 155, 0.9);
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    overflow-y: auto;
    overflow-x: hidden;
}

.iwb-toolbar-note .note-header {
    height: 20px;
    line-height: 20px;
    background: url('resources/wb_panel_note_icon.svg') no-repeat;
    background-size: 15px;
    background-position: 3px;
    cursor: move;
    background-color: rgba(255, 204, 0, 0.5);
}

.iwb-toolbar-note .note-body {
    padding: 5px;
    font-size: 0.8em;
    word-wrap: break-word;
    overflow-wrap: break-word;
    white-space: pre;
}

.iwb-toolbar-note textarea {
    width: 160px;
    margin: auto;
    box-sizing: border-box;
    min-height: 70px;
    display: block;
    resize: vertical;
}

.iwb-toolbar-note .save {
    float: right;
    right: 3px;
    position: relative;
}

.iwb-toolbar-note .note-date {
    font-style: italic;
    font-size: 12px;
    width: 80%;
    position: relative;
    left: 23px;
    float: left;
    color: #D58A00;
}

.iwb-toolbar-note .note-close {
    width: 10%;
    right: 5px;
    float: right;
    position: relative;
    cursor: pointer;
    text-align: right;
}

.iwb-toolbar-note .note-close.hovered {
    color: #696969;
}

.iwb-toolbar-custom-script {
    top: 0px;
    width: 185px;
    min-height: 120px;
    max-height: 350px;
    position: absolute;
    background-color: rgba(255, 255, 155, 0.9);
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    overflow-y: auto;
    overflow-x: hidden;
}

.iwb-toolbar-custom-script .custom-script-close.hovered {
    color: #696969;
}

.iwb-toolbar-floating-image {
    display: none;
}

.iwb-toolbar-panel .button .tooltip {
    display: none;
    opacity: 1;
    position: absolute;
    font-size: 8px;
    line-height: 8px;
    min-width: 30px;
    padding: 3px;
    border-radius: 2px;
    background-color: rgba(255, 255, 255, 0.5);
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.6);
}

.iwb-toolbar-panel.right .button .tooltip,
.iwb-toolbar-panel.left .button .tooltip{
    top: -20px;
}

.iwb-toolbar-panel.up .button .tooltip,
.iwb-toolbar-panel.down .button .tooltip{
    left: -30px;
}

.iwb-toolbar-panel .button.default .tooltip span:after {
    content: 'Default Mode';
}

.iwb-toolbar-panel .button.close .tooltip span:after {
    content: 'Close';
}

.iwb-toolbar-panel .button.open .tooltip span:after {
    content: 'Open';
}

.iwb-toolbar-panel .button.zoom .tooltip span:after {
    content: 'Zoom';
}

.iwb-toolbar-panel .button.pen .tooltip span:after {
    content: 'Pen';
}

.iwb-toolbar-panel .button.marker .tooltip span:after {
    content: 'Marker';
}

.iwb-toolbar-panel .button.eraser .tooltip span:after {
    content: 'Eraser';
}

.iwb-toolbar-panel .button.clock .tooltip span:after {
    content: 'Add Clock';
}

.iwb-toolbar-panel .button.stopwatch .tooltip span:after {
    content: 'Add Stopwatch';
}

.iwb-toolbar-panel .button.note .tooltip span:after {
    content: 'Add Note';
}

.iwb-toolbar-panel .button.custom-script .tooltip span:after {
    content: 'Run custom script';
}

.iwb-toolbar-panel .button.hide-area .tooltip span:after {
    content: 'Hide Area';
}

.iwb-toolbar-panel .button.stand-area .tooltip span:after {
    content: 'Stand Area';
}

.iwb-toolbar-panel .button.reset .tooltip span:after {
    content: 'Undo All';
}

.iwb-toolbar-panel .button.reset-one .tooltip span:after {
    content: 'Undo';
}

.iwb-toolbar-panel .button.redo-one .tooltip span:after {
    content: 'Redo';
}

.iwb-toolbar-panel .button.floating-image .tooltip span:after {
    content: 'Rulers';
}

.iwb-toolbar-panel .button.color .tooltip span:after {
    content: 'Color';
}

.iwb-toolbar-panel .button.thickness .tooltip span:after {
    content: 'Thickness';
}

#iwb_tmp_canvas {
    -ms-touch-action: none;
}

canvas {
    -ms-touch-action: none;
}

.confirmation-remove-note &gt; span:after {
     content: 'Are you sure to remove this note?';
}

.confirmation-remove-note .yes-button &gt; span:after {
    content: 'Yes';
}

.confirmation-remove-note .no-button &gt; span:after {
    content: 'No';
}

.confirmation-remove-note .no-button {
    float: right;
}

.confirmation-remove-note .yes-button {
    float: left;
}

.confirmation-remove-note .button {
    padding: 10px;
    border-radius: 5px;
    border: none;
    width: 50px;
    text-align: center;
    box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
    background: #FFF;
    color: #000;
}

.confirmation-remove-note .ui-helper-clearfix {
    margin-top: 10px;
}

.confirmation-remove-note {
    min-height: 50px;
    padding: 10px;
    display: none;
    width: 170px;
    left: 50%;
    margin-left: -100px;
    position: absolute;
    background-color: #63BAFF;
    color: #FFF;
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    font-size: 14px;
    z-index: 1002;
    padding: 30px 50px;
    text-align: center;
}

.confirmation-remove-clock &gt; span:after {
    content: 'Are you sure to remove this clock?';
}

.confirmation-remove-clock .yes-button &gt; span:after {
    content: 'Yes';
}

.confirmation-remove-clock .no-button &gt; span:after {
    content: 'No';
}

.confirmation-remove-clock .no-button {
    float: right;
}

.confirmation-remove-clock .yes-button {
    float: left;
}

.confirmation-remove-clock .button {
    padding: 5px;
    border-radius: 5px;
    border: 1px solid #696969;
    width: 50px;
    text-align: center;
    box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
}

.confirmation-remove-clock .ui-helper-clearfix {
    margin-top: 10px;
}

.confirmation-remove-clock {
    min-height: 50px;
    padding: 10px;
    display: none;
    width: 200px;
    left: 50%;
    margin-left: -100px;
    position: absolute;
    background-color: rgba(245, 245, 245, 0.7);
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    font-size: 0.7em;
    z-index: 1002;
}

.confirmation-remove-stopwatch &gt; span:after {
    content: 'Are you sure to remove this stopwatch?';
}

.confirmation-remove-stopwatch .yes-button &gt; span:after {
    content: 'Yes';
}

.confirmation-remove-stopwatch .no-button &gt; span:after {
    content: 'No';
}

.confirmation-remove-stopwatch .no-button {
    float: right;
}

.confirmation-remove-stopwatch .yes-button {
    float: left;
}

.confirmation-remove-stopwatch .button {
    padding: 5px;
    border-radius: 5px;
    border: 1px solid #696969;
    width: 50px;
    text-align: center;
    box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
    cursor: pointer;
}

.confirmation-remove-stopwatch .ui-helper-clearfix {
    margin-top: 10px;
}

.confirmation-remove-stopwatch {
    min-height: 50px;
    padding: 10px;
    display: none;
    width: 200px;
    left: 50%;
    margin-left: -100px;
    position: absolute;
    background-color: rgba(245, 245, 245, 0.7);
    border-radius: 4px;
    box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
    font-size: 0.7em;
    z-index: 1002;
}

.ui-helper-clearfix:before, .ui-helper-clearfix:after {
    content: "";
    display: table;
    height: 10px;
}

.iwb-toolbar-panel.up .ui-helper-clearfix:before,
.iwb-toolbar-panel.down .ui-helper-clearfix:before,
.iwb-toolbar-panel.up .ui-helper-clearfix:after,
.iwb-toolbar-panel.down .ui-helper-clearfix:after {
    height: 0px;
    width: 10px;
}

#iwb_tmp_canvas {
    position: absolute;
    left: 0px; right: 0;
    bottom: 0; top: 0;
    cursor: crosshair;
}

.closed-hide {
    display: none !important;
}

.ic_page_panel.iwb-zoom-in {
    cursor: zoom-in;
    cursor: -moz-zoom-in;
    cursor: -webkit-zoom-in;
}

.ic_page_panel.iwb-zoom-out {
    cursor: pointer;
}
</css><view>&lt;div class="confirmation-remove-note"&gt;
    &lt;span&gt;&lt;/span&gt; &lt;!-- text goes here --&gt;
    &lt;div class="ui-helper-clearfix"&gt;
        &lt;div class="yes-button button"&gt;
            &lt;span&gt;&lt;/span&gt; &lt;!-- text goes here --&gt;
        &lt;/div&gt;
        &lt;div class="no-button button"&gt;
            &lt;span&gt;&lt;/span&gt; &lt;!-- text goes here --&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;div class="iwb-toolbar-panel"&gt;
    &lt;div class="button first open" data-name="open"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button first close hidden" data-name="close"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button default hidden" data-name="default"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button zoom hidden" data-name="zoom"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button pen hidden" data-name="pen"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button marker hidden" data-name="marker"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button eraser hidden" data-name="eraser"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button hide-area hidden" data-name="hide-area"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button stand-area hidden" data-name="stand-area"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button-separator hidden"&gt;&lt;/div&gt;
    &lt;div class="button color hidden" data-name="color"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button thickness hidden" data-name="thickness"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button-separator hidden" &gt;&lt;/div&gt;
    &lt;div class="button reset-one hidden" data-name="reset-one"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button redo-one hidden" data-name="redo-one"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button reset hidden" data-name="reset"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button-separator hidden" &gt;&lt;/div&gt;
    &lt;div class="button note hidden" data-name="note"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button floating-image hidden" data-name="floating-image"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button clock hidden" data-name="clock"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button stopwatch hidden" data-name="stopwatch"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="button custom-script hidden" data-name="custom-script"&gt;&lt;div class="tooltip"&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;div class="bottom-panel ui-helper-clearfix bottom-panel-color"&gt;
        &lt;div class="container"&gt;
            &lt;div class="button-drawing-details color-blue" color="blue"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-green" color="green"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-yellow" color="yellow"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-orange" color="orange"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-red" color="red"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-violet" color="violet"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-black" color="black"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-white" color="white"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="bottom-panel ui-helper-clearfix bottom-panel-thickness"&gt;
        &lt;div class="container"&gt;
            &lt;div class="button-drawing-details thickness-1" thickness="1"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-2" thickness="2"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-3" thickness="3"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-4" thickness="4"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="bottom-panel ui-helper-clearfix bottom-panel-floating-image"&gt;
        &lt;div class="container"&gt;
            &lt;div class="button-floating-image button-floating-image-1" index="0"&gt;&lt;/div&gt;
            &lt;div class="button-floating-image button-floating-image-2" index="1"&gt;&lt;/div&gt;
            &lt;div class="button-floating-image button-floating-image-3" index="2"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="iwb-toolbar-panel"&gt;
    &lt;div class="button first open"&gt;&lt;/div&gt;
    &lt;div class="button first close hidden"&gt;&lt;/div&gt;
    &lt;div class="button default hidden"&gt;&lt;/div&gt;
    &lt;div class="button zoom hidden"&gt; &lt;/div&gt;
    &lt;div class="button pen hidden"&gt;&lt;/div&gt;
    &lt;div class="button marker hidden"&gt;&lt;/div&gt;
    &lt;div class="button note hidden"&gt;&lt;/div&gt;
    &lt;div class="button clock hidden"&gt;&lt;/div&gt;
    &lt;div class="button stopwatch hidden"&gt;&lt;/div&gt;
    &lt;div class="button custom-script hidden"&gt;&lt;/div&gt;
    &lt;div class="button hide-area hidden"&gt;&lt;/div&gt;
    &lt;div class="button stand-area hidden"&gt;&lt;/div&gt;
    &lt;div class="button eraser hidden"&gt;&lt;/div&gt;
    &lt;div class="button remove hidden"&gt;&lt;/div&gt;
    &lt;div class="button-separator hidden"&gt;&lt;/div&gt;
    &lt;div class="button color hidden"&gt;&lt;/div&gt;
    &lt;div class="button thickness hidden"&gt;&lt;/div&gt;
    &lt;div class="bottom-panel ui-helper-clearfix bottom-panel-color"&gt;
        &lt;div class="container"&gt;
            &lt;div class="button-drawing-details color-blue" color="blue"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-green" color="green"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-yellow" color="yellow"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-orange" color="orange"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-red" color="red"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-violet" color="violet"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-black" color="black"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details color-white" color="white"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="bottom-panel ui-helper-clearfix bottom-panel-thickness"&gt;
        &lt;div class="container"&gt;
            &lt;div class="button-drawing-details thickness-1" thickness="1"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-2" thickness="2"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-3" thickness="3"&gt;&lt;/div&gt;
            &lt;div class="button-drawing-details thickness-4" thickness="4"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonIWB_Toolbar_create() {

    /*
     * KNOWN ISSUES:
     *       Drawing with zoom:
     *          Because zoom option (provided with zoom.js library) is based on CSS properties 'zoom' and
     *          '-moz-transform' drawing in such mode is not possible. IWB Toolbar panel is hidden when user activates
     *          zoom option and it's displayed again after zooming out.
     *
     *       Preventing modules and addons click handlers execution in zoom mode:
     *          Each module and addon can register unlimited number of click handlers to its internal elements. When
     *          zoom mode is activated, IWB Toolbar needs to prevent execution of those handlers. For instance,
     *          TextAudio binds a handler to span elements. Those handlers needs to be removed for as long as zoom
      *         mode is active and reinstated when zoom mode is deactivated.
      *
      *      Incomplete erasing on Android:
      *      Issue occurs sometimes without known reason. The helpful solution was workaround redrawing canvas.
     */

    var presenter = function() {};

    presenter.noteObjects = [];

    presenter._kinetic = {};
    presenter._kinetic.images = [];
    presenter._kinetic.rotateObj = [];
    presenter._kinetic.moveObj = [];
    presenter._kinetic.rotateIcon = [];
    presenter._kinetic.moveIcon = [];
    presenter._kinetic.imageObj = [];
    presenter._hoveredButtons = [];
    presenter._iwb_buttons = [];
    presenter._setState = {};
    presenter._setState.images = [];

    presenter.stateStack = null;

    presenter._stopwatchTimer = null;
    presenter._stopwatch = null;

    presenter._clockwatch = null;
    presenter._clockwatchTimer = null;

    presenter.buttonClicked = false;
    presenter.isZoomActive = false;
    presenter.areZoomEventHandlersAttached = false;

    var DEFAULT_COLOR = '#0fa9f0';
    var GAP_VALUE = 10;
    var STOPWATCH_OFFSET = 60;
    var CLOCK_OFFSET = 60;
    var NOTE_OFFSET = 120;
    presenter.activeButton = '';
    presenter.activeFunction;
    presenter.isRecklicked = false;
    const CUSTOM_BUTTON_NAME = 'custom-script';
    presenter.customButtonActive = false;
    presenter.isCommandSourceToDeselectCustomButton = false;

    presenter.points = [];
    presenter.mouse = {x: 0, y: 0};

    presenter.trueClosedPanelSize = {};

    presenter.data = {
        defaultPenWidth: 1,
        penColor: DEFAULT_COLOR,
        markerColor: '#ffff99',
        markerThickness: 10,
        eraserThickness: 20
    };

    presenter.playerController = null;
    presenter.eventBus = null;

    presenter.areas = [];
    presenter.clocks = [];
    presenter.stopwatches = [];
    presenter.currentLineColor = DEFAULT_COLOR;
    presenter.currentLineWidth = presenter.data.defaultPenWidth;
    presenter.isPointerDown = false;
    presenter.lastMousePosition = {};
    presenter.floatingImageGroups = {};
    presenter.currentFloatingImageIndex = 0;
    presenter.textAudioEvents = [];

    presenter.penUsed = false;
    presenter.markerUsed = false;
    presenter.isPointerEventSupported = !!window.PointerEvent;

    presenter.notesCounter = 0;
    presenter.POPUP_TYPE = {
        STOPWATCH: 'stopwatch',
        CLOCK: 'clock',
        NOTE: 'note'
    };

    presenter.zoomConfiguration = {
        initialWindowHeight: 0,
        initialNotScaledOffset: 0,
        playerInitialLeftOffset: 0,
        viewLeftOffset: 0
    };

    function getCorrectObject(val) {
        return {
            isValid: true,
            value: val
        };
    }

    function getErrorObject(ec) {
        return {
            isValid: false,
            errorCode: ec
        };
    }

    function addZero(val) {
        return (val &gt; 9 ? '' : '0') + val;
    }

    function getPoint(x, y) {
        return {
            x: x,
            y: y
        };
    }

    presenter.DEFAULT_FLOATING_IMAGE = {
        0: 'it_ruler.png',
        1: 'it_setsquare.png',
        2: 'it_protractor.png'
    };

    presenter.FLOATING_IMAGE_MODE = {
        ROTATE: 1,
        MOVE: 2
    };

    presenter.DRAW_MODE = {
        MARKER: 1,
        PEN: 2,
        ERASER: 3,
        STAND_AREA: 4,
        HIDE_AREA: 5,
        NONE : 6
    };

    presenter.drawMode = presenter.DRAW_MODE.NONE;
    presenter.floatingImageMode = presenter.FLOATING_IMAGE_MODE.MOVE;

    presenter.DRAWING_DATA = {
        'color' : {
            'black': '#000',
            'white': '#fff',
            'yellow':'#FFFF66',
            'red': '#cf304b',
            'orange': '#FF9900',
            'blue': '#0fa9f0',
            'violet': '#990099',
            'green': '#05fa98'
        },
        'thickness' : {
            'pen' : {
                '1': 1,
                '2': 3,
                '3': 5,
                '4': 7
            },
            'marker' : {
                '1': 10,
                '2': 15,
                '3': 20,
                '4': 25
            }
        }
    };

    function getCurrentGroup() {
        return presenter.floatingImageGroups[presenter.currentFloatingImageIndex];
    }

    function getCurrentImage() {
        return presenter.floatingImageGroups[presenter.currentFloatingImageIndex].children[0];
    }

    function getCurrentMoveIcon() {
        return presenter.floatingImageGroups[presenter.currentFloatingImageIndex].children[1];
    }

    function getCurrentRotateIcon() {
        return presenter.floatingImageGroups[presenter.currentFloatingImageIndex].children[2];
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('ResizeWindow', this);
    };

    presenter.onEventReceived = function(eventName, eventData) {
        if (eventName == 'PageLoaded' &amp;&amp; eventData.source == 'header') {
            presenter.headerLoadedDeferred.resolve();
        }

        if (eventName == 'PageLoaded') {
            presenter.loadWindowSize();
        }

        if (eventName == "ResizeWindow") {
            var newPlayerLeftOffset = $("#_icplayer").offset().left;
            var viewLeftOffset = presenter.zoomConfiguration.viewLeftOffset;
            var playerInitialLeftOffset = presenter.zoomConfiguration.playerInitialLeftOffset;
            var newViewLeftOffset = newPlayerLeftOffset - playerInitialLeftOffset + viewLeftOffset;
            presenter.$panel.offset({left: newViewLeftOffset});
        }
    };

    presenter.loadWindowSize = function(){
        presenter.zoomConfiguration.initialWindowHeight = window.iframeSize.windowInnerHeight;
        presenter.zoomConfiguration.initialNotScaledOffset = window.iframeSize.notScaledOffset;

        // you must repeat the data reading when they are not loaded correctly
        if (presenter.zoomConfiguration.initialWindowHeight === 0 || isNaN(presenter.zoomConfiguration.initialNotScaledOffset))
            setTimeout(function (e) {
                presenter.loadWindowSize();
            }, 200);
    };

    presenter.ERROR_CODES = {
        'E01': 'Width can NOT be negative.',
        'E02': 'Height can NOT be negative.',
    };

    presenter.closePanel = function IWB_Toolbar_closePanel(doAnimation) {
        if (!presenter.$panel.hasClass('animationInProgress')) {
            window.savedPanel.isOpen = false;

            presenter.$bottomPanels.addClass('closed-hide');
            presenter.$buttonsExceptOpen.addClass('hidden');
            presenter.$panel.children('.button-separator').hide();

            var config = {};
            var openedPanelPosition = {
                left: presenter.$panel.position().left,
                top: presenter.$panel.position().top
            };
            let positionToSave = {};
            $.extend(true, positionToSave, openedPanelPosition);

            var newLeft = openedPanelPosition.left + presenter.config.widthWhenOpened - presenter.trueClosedPanelSize.width;
            var newTop = openedPanelPosition.top + presenter.config.widthWhenOpened - presenter.trueClosedPanelSize.height;
            if (presenter.config.expansionDirection == "right" || presenter.config.expansionDirection == "left") {
                config.width = presenter.trueClosedPanelSize.width + 'px';
                if (presenter.config.expansionDirection == "left") {
                    config.left = newLeft + 'px';
                    positionToSave.left = newLeft;
                }
            } else {
                config.height = presenter.trueClosedPanelSize.height + 'px';
                if (presenter.config.expansionDirection == "up") {
                    config.top = newTop + 'px';
                    positionToSave.top = newTop;
                }
            }

            if (presenter.config.panelPosition === 'fixed') {
                positionToSave.top -= presenter.$pagePanel.offset().top;
                positionToSave.left -= presenter.$pagePanel.offset().left;
            }

            window.savedPanel.position = {
                top: positionToSave.top,
                left: positionToSave.left
            };

            if (doAnimation) {
                presenter.$panel.addClass('animationInProgress');
                presenter.$panel.animate(config, 1000, presenter._closePanelShow);
            } else {
                presenter._closePanelShow();
                config.hasOwnProperty("width") &amp;&amp; presenter.$panel.css("width", config.width);
                config.hasOwnProperty("height") &amp;&amp; presenter.$panel.css("height", config.height);
                config.hasOwnProperty("left") &amp;&amp; presenter.$panel.css("left", config.left);
                config.hasOwnProperty("top") &amp;&amp; presenter.$panel.css("top", config.top);
            }
        }
    };

    presenter._openPanelShow = function IWB_Toolbar_openPanelShow() {
        presenter.$buttonsExceptOpen.removeClass('hidden');
        presenter.$panel.children('.button.open').hide();
        presenter.$panel.children('.button-separator').show();
        presenter.$panel.removeClass('animationInProgress');
        presenter.$panel.addClass('opened');
        presenter.$bottomPanels.removeClass('closed-hide');
        presenter.toogleMasks();
    };

    presenter._closePanelShow = function IWB_Toolbar_closePanelShow() {
        presenter.$panel.children('.button.open').show();
        presenter.$panel.removeClass('animationInProgress');
        presenter.$panel.removeClass('opened');
    };

    presenter.openPanel = function IWB_Toolbar_openPanel(doAnimation) {
        window.savedPanel.isOpen = true;

        var config = {};
        var closedPanelPosition = {
            left: presenter.$panel.position().left,
            top: presenter.$panel.position().top
        };

        var newLeft = closedPanelPosition.left + presenter.trueClosedPanelSize.width - presenter.config.widthWhenOpened;
        var newTop = closedPanelPosition.top + presenter.trueClosedPanelSize.height - presenter.config.widthWhenOpened;
        if (presenter.config.expansionDirection == "right" || presenter.config.expansionDirection == "left") {
            config.width = presenter.config.widthWhenOpened + "px";
            if (presenter.config.expansionDirection == "left") {
                config.left = newLeft + "px";
            }
        } else {
            config.height = presenter.config.widthWhenOpened + "px";
            if (presenter.config.expansionDirection == "up") {
                config.top = newTop + "px";
            }
        }

        if (doAnimation) {
            presenter.$panel.addClass('animationInProgress');
            presenter.$panel.animate(config, 1000, presenter._openPanelShow);
        } else {
            presenter._openPanelShow();
            config.hasOwnProperty("width") &amp;&amp; presenter.$panel.css("width", config.width);
            config.hasOwnProperty("height") &amp;&amp; presenter.$panel.css("height", config.height);
            config.hasOwnProperty("left") &amp;&amp; presenter.$panel.css("left", config.left);
            config.hasOwnProperty("top") &amp;&amp; presenter.$panel.css("top", config.top);
        }
    };

    function setOverflowWorkAround(turnOn) {
        if (!MobileUtils.isAndroidWebBrowser(window.navigator.userAgent)) { return false; }

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {

            presenter.$pagePanel.find('.iwb-toolbar-mask').parents("*").each(function() {
                var overflow = null;
                if (turnOn) {
                    $(this).attr("data-overflow", $(this).css("overflow"));
                    $(this).css("overflow", "visible");
                } else {
                    overflow = $(this).attr("data-overflow");
                    if (overflow !== "") {
                        $(this).css("overflow", overflow);
                    }
                    $(this).removeAttr("data-overflow");
                }
            });

        }

        return true;
    }

    presenter.IWBDraw = function(canvas, ctx, mousePosition) {
        const grad = ctx.createLinearGradient(0, 0, canvas[0].width, 0);
        grad.addColorStop(0, presenter.currentLineColor);
        grad.addColorStop(1, presenter.currentLineColor);

        ctx.lineWidth = presenter.drawMode === presenter.DRAW_MODE.ERASER ? presenter.currentEraserThickness : presenter.currentLineWidth;
        ctx.strokeStyle = grad;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';

        ctx.beginPath();
        ctx.moveTo(presenter.lastMousePosition.x, presenter.lastMousePosition.y);
        ctx.lineTo(mousePosition.x, mousePosition.y);
        ctx.stroke();
    };

    function getCursorPosition(e) {
        var canvas;

        if(e.target.id == "iwb_tmp_canvas") {
            canvas = presenter.$view.parent().find('.selecting').find('#iwb_tmp_canvas')[0];
        } else {
            canvas = presenter.$canvas[0];
        }

        var rect = canvas.getBoundingClientRect();
        var canvasOffsetLeft = $(canvas).offset().left;

        if(presenter.standHideAreaClicked){
            canvasOffsetLeft = 0;
        }

        const clientX = e.originalEvent ? e.originalEvent.clientX : e.clientX;
        const clientY = e.originalEvent ? e.originalEvent.clientY : e.clientY;
        if (clientX) {
            return getPoint(
                parseInt(clientX - rect.left, 10),
                parseInt(clientY - rect.top, 10)
            );
        }

        var t = event.targetTouches[0] || event.touches[0] || event.changedTouches[0];
        return getPoint(
            parseInt(t.pageX - canvasOffsetLeft, 10),
            parseInt(t.pageY - $(canvas).offset().top, 10)
        );
    }

    function changeDrawingType(button) {
        var activeButton = presenter.$pagePanel.find('.clicked');

        if ($(button).parent().parent().hasClass('bottom-panel-thickness')) { // is changing thickness
            var thickness = $(button).attr('thickness'),
                drawingType = activeButton.hasClass('pen') ? 'pen' : 'marker';
            presenter.data.defaultPenWidth = 1;
            presenter.changeThickness(presenter.DRAWING_DATA['thickness'][drawingType][thickness], button);
            presenter.buttonThickness = button;
        } else {
            var color = $(button).attr('color');
            presenter.changeColor(presenter.DRAWING_DATA['color'][color], button);
            presenter.buttonColor = button;
        }
    }

    function openBottomPanel(button) {
        presenter.$pagePanel.find('.bottom-panel').hide();

        var panel;
        if ($(button).hasClass('color')) {
            panel = presenter.$pagePanel.find('.bottom-panel-color');
        } else if ($(button).hasClass('thickness')) {
            panel = presenter.$pagePanel.find('.bottom-panel-thickness');
        } else {
            panel = presenter.$pagePanel.find('.bottom-panel-floating-image');
        }

        if(panel.hasClass('bottom-panel-floating-image')){
            if(panel.attr('isHidden') == '0'){
                panel.hide();
                panel.attr('isHidden', '1');
                return;
            }
            if (panel.is(':visible')) {
                panel.hide();
                panel.attr('isHidden', '1');
            } else {
                panel.show();
                panel.attr('isHidden', '0');
            }
        }else{
            if (panel.is(':visible')) {
                panel.hide();
            } else {
                panel.show();
            }
        }
    }

    function applyDoubleTapHandler($element, callback) {
        var lastEvent = null,
            tapsCounter = 0;

        $element.on('touchstart', function(e) {
            lastEvent = e.evt || e;
        });

        $element.on('touchend', function(e) {
            if (lastEvent.type == 'touchstart') {
                tapsCounter++;

                if (tapsCounter == 2) {
                    callback();
                    tapsCounter = 0;
                }
            }
            lastEvent = e.evt || e;
        });

        $element.on('touchmove', function(e) {
            lastEvent = e.evt || e;
        });
    }
    
    function getTouchStartOrMouseDownEventName() {
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            return 'touchstart';
        }
        return EventsUtils.PointingEvents.TYPES.DOWN;
    }

    presenter.onMobilePaint = function(e) {
        var iwb_tmp_canvas;
        iwb_tmp_canvas = presenter.markerTmpCanvas;

        e.preventDefault();
        e.stopPropagation();

        var x = e.targetTouches[0].pageX - $(iwb_tmp_canvas).offset().left;
        var y = e.targetTouches[0].pageY - $(iwb_tmp_canvas).offset().top;

        presenter.mouse.x = x;
        presenter.mouse.y = y;
        presenter.onPaint(e);
    };

    presenter.onPaint = function(e) {
        var iwb_tmp_canvas, tmp_ctx;
        iwb_tmp_canvas = presenter.markerTmpCanvas;
        tmp_ctx = presenter.markerTmpCtx;
        tmp_ctx.globalAlpha = 0.4;

        tmp_ctx.lineWidth = presenter.currentMarkerThickness;
        tmp_ctx.lineJoin = 'round';
        tmp_ctx.lineCap = 'round';
        tmp_ctx.strokeStyle = presenter.currentLineColor;
        tmp_ctx.fillStyle = presenter.currentLineColor;

        presenter.points.push({x: presenter.mouse.x, y: presenter.mouse.y});

        if (presenter.points.length &lt; 3) {
            var b = presenter.points[0];
            tmp_ctx.beginPath();
            tmp_ctx.arc(b.x, b.y, tmp_ctx.lineWidth / 2, 0, Math.PI * 2, !0);
            tmp_ctx.fill();
            tmp_ctx.closePath();
        } else {
            tmp_ctx.clearRect(0, 0, iwb_tmp_canvas.width, iwb_tmp_canvas.height);

            tmp_ctx.beginPath();
            tmp_ctx.moveTo(presenter.points[0].x, presenter.points[0].y);

            for (var i = 1; i &lt; presenter.points.length - 2; i++) {
                var c = (presenter.points[i].x + presenter.points[i + 1].x) / 2;
                var d = (presenter.points[i].y + presenter.points[i + 1].y) / 2;

                tmp_ctx.quadraticCurveTo(presenter.points[i].x, presenter.points[i].y, c, d);
            }

            tmp_ctx.quadraticCurveTo(
                presenter.points[i].x,
                presenter.points[i].y,
                presenter.points[i + 1].x,
                presenter.points[i + 1].y
            );
            tmp_ctx.stroke();
        }
    };

    presenter.onTouchStartCallback = function (event) {
        event.preventDefault();
        event.stopPropagation();

        setOverflowWorkAround(true);

        presenter.onMobilePaint(event);
        presenter.markerTmpCanvas.addEventListener('touchmove', presenter.onMobilePaint);
    };

    function saveMarkerDataUrl() {
        presenter.markerDataUrl = presenter.$markerCanvas[0].toDataURL('image/png');
    }

    presenter.onTouchEndEventCallback = function (event) {
        event.stopPropagation();

        setOverflowWorkAround(false);

        presenter.markerUsed = true;

        presenter.markerTmpCanvas.removeEventListener('touchmove', presenter.onMobilePaint, false);
        presenter.markerCtx.drawImage(presenter.markerTmpCanvas, 0, 0);
        presenter.markerTmpCtx.clearRect(0, 0, presenter.markerTmpCanvas.width, presenter.markerTmpCanvas.height);

        presenter.points = [];
        saveMarkerDataUrl();
    };

    presenter.markerPointerDownHandler = function IWB_Toolbar_markerPointerDownHandler(event) {
        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
            return;
        }
        presenter.isPointerDown = true;
        setOverflowWorkAround(true);

        presenter.markerTmpCanvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onPaint, false);

        presenter.points.push({
            x: typeof event.offsetX !== 'undefined' ? event.offsetX : event.layerX,
            y: typeof event.offsetY !== 'undefined' ? event.offsetY : event.layerY
        });
    };

    presenter.markerPointerMoveHandler = function IWB_Toolbar_markerPointerMoveHandler(event) {
        if (!presenter.isPointerDown) {
            return;
        }

        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event) || presenter.drawMode !== presenter.DRAW_MODE.MARKER) {
            return;
        }

        presenter.mouse.x = typeof event.offsetX !== 'undefined' ? event.offsetX : event.layerX;
        presenter.mouse.y = typeof event.offsetY !== 'undefined' ? event.offsetY : event.layerY;
    };

    presenter.markerPointerUpHandler = function IWB_Toolbar_markerPointerUpHandler(event) {
        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
            return;
        }

        if (presenter.isPointerDown) {
            presenter.markerUsed = true;
        }
        presenter.isPointerDown = false;
        setOverflowWorkAround(false);

        presenter.markerTmpCanvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.onPaint, false);
        presenter.markerCtx.drawImage(presenter.markerTmpCanvas, 0, 0);
        presenter.markerTmpCtx.clearRect(0, 0, presenter.markerTmpCanvas.width, presenter.markerTmpCanvas.height);

        presenter.points = [];
        saveMarkerDataUrl();

        presenter.pushStateToStack();
    };

    presenter.markerDrawingLogic = function IWB_Toolbar_markerDrawingLogic() {
        $(presenter.markerTmpCanvas).off();

        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported('touchstart')) {
            presenter.markerTmpCanvas.addEventListener(EventsUtils.PointingEvents.TYPES.DOWN, presenter.markerPointerDownHandler, false);
            presenter.markerTmpCanvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, presenter.markerPointerMoveHandler, false);
            presenter.markerTmpCanvas.addEventListener(EventsUtils.PointingEvents.TYPES.UP, presenter.markerPointerUpHandler, false);
            presenter.markerTmpCanvas.addEventListener(EventsUtils.PointingEvents.TYPES.LEAVE, presenter.markerPointerUpHandler, false);
        } else {
            presenter.markerTmpCanvas.addEventListener('touchstart', presenter.onTouchStartCallback, false);
            presenter.markerTmpCanvas.addEventListener('touchend', presenter.onTouchEndEventCallback, false);
        }
    };

    presenter.penPointerDownHandler = function IWB_Toolbar_penPointerDownHandler(event) {
        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
            return;
        }
        presenter.lastMousePosition = getCursorPosition(event);
        presenter.isPointerDown = true;
        setOverflowWorkAround(true);
    };

    presenter.penPointerMoveHandler = function IWB_Toolbar_penPointerMoveHandler(event) {
        if (!presenter.isPointerDown) {
            return;
        }

        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
            return;
        }

        if (presenter.drawMode === presenter.DRAW_MODE.PEN) {
            presenter.IWBDraw(presenter.$canvas, presenter.ctx, getCursorPosition(event));
        } else if (presenter.drawMode === presenter.DRAW_MODE.ERASER) {
            presenter.IWBDraw(presenter.$markerCanvas, presenter.markerCtx, getCursorPosition(event));
            presenter.IWBDraw(presenter.$canvas, presenter.ctx, getCursorPosition(event));

            var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
            if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {
                $('canvas').css('opacity', '0.99');

                setTimeout(function() {
                    $('canvas').css('opacity', '1');
                }, 5);
            }
        }
        presenter.lastMousePosition = getCursorPosition(event);
    };

    presenter.penPointerUpHandler = function IWB_Toolbar_penPointerUpHandler(event) {
        event.stopPropagation();
        event.preventDefault();
        if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
            return;
        }
        if (presenter.isPointerDown) {
            presenter.penUsed = true;
        }
        presenter.isPointerDown = false;
        setOverflowWorkAround(false);
        presenter.penDataURL = presenter.$canvas[0].toDataURL('image/png');
        if (presenter.drawMode === presenter.DRAW_MODE.ERASER &amp;&amp; presenter.markerUsed) {
            saveMarkerDataUrl();
        }

        presenter.pushStateToStack();
    };

    presenter.drawingLogic = function IWB_Toolbar_drawingLogic() {
        $(presenter.markerTmpCanvas).off();

        let downEventName, moveEventName, upEventName;
        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported('touchstart')) {
            downEventName = EventsUtils.PointingEvents.TYPES.DOWN;
            moveEventName = EventsUtils.PointingEvents.TYPES.MOVE;
            upEventName = EventsUtils.PointingEvents.TYPES.UP;
        } else {
            downEventName = "touchstart";
            moveEventName = "touchmove";
            upEventName = "touchend";
        }

        const canvases = [presenter.$canvas[0], presenter.$markerCanvas[0]];
        for (let i = 0; i &lt; canvases.length; i++) {
            canvases[i].addEventListener(downEventName, presenter.penPointerDownHandler, false);
            canvases[i].addEventListener(moveEventName, presenter.penPointerMoveHandler, false);
            canvases[i].addEventListener(upEventName, presenter.penPointerUpHandler, false);
        }
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeEnableUndoRedo(model);
        upgradedModel = presenter.upgradeDefaultZoom(upgradedModel);
        upgradedModel = presenter.upgradeExpansionDirection(upgradedModel);
        upgradedModel = presenter.upgradePopupsDirection(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeEnableUndoRedo = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel['enableUndoRedo']) {
            upgradedModel['enableUndoRedo'] = 'false';
        }
        return upgradedModel;
    };

    presenter.upgradeExpansionDirection = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["expansionDirection"]) {
            upgradedModel["expansionDirection"] = "right";
        }
        return upgradedModel;
    };

    presenter.upgradePopupsDirection = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["verticalPopupDirection"]) {
            upgradedModel["verticalPopupDirection"] = "right";
        }

        if (!upgradedModel["horizontalPopupDirection"]) {
            upgradedModel["horizontalPopupDirection"] = "down";
        }

        return upgradedModel;
    };

    presenter.upgradeDefaultZoom = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel['disableModuleZoom']) {
            upgradedModel['disableModuleZoom'] = 'false';
        }

        if (!upgradedModel['defaultZoom']) {
            upgradedModel['defaultZoom'] = '';
        }

        return upgradedModel;
    };

    presenter.setBasicConfiguration = function (view, model) {
        presenter.$view = $(view);
        presenter.$panel = $(view).find('.iwb-toolbar-panel');
        presenter.$panel.attr('id', model['ID'] + '-panel');
        presenter.$defaultThicknessButton = presenter.$panel.find('.thickness-1');
        presenter.$defaultColorButton = presenter.$panel.find('.color-blue');
        presenter.isInFrame = window.parent.location != window.location;
        presenter.$buttonsExceptOpen = presenter.$panel.children('.button:not(.open)');
        presenter.buttonWidth = presenter.$buttonsExceptOpen.width();

        var display = $(view).css('display');
        if (display &amp;&amp; display.length &gt; 0 &amp;&amp; display != "block") {
            presenter.$panel.css('display', display);
        }

        presenter.$view.parent().append(presenter.$panel);

        presenter.$view.disableSelection();
        presenter.$removeConfirmationBox = presenter.$view.find('.confirmation-remove-note');
        presenter.$removeConfirmationBox.attr('id', 'confirmationBox-' + model['ID']);

        presenter.$pagePanel = presenter.$view.parent().parent('.ic_page_panel');
        presenter.$pagePanel.find('.ic_page').append(presenter.$removeConfirmationBox);
        presenter.$pagePanel.find('.ic_page').append(presenter.$removeConfirmationBoxClock);
        presenter.$pagePanel.find('.ic_page').append(presenter.$removeConfirmationBoxStopwatch);
        presenter.changeCursor('default');


        presenter.$removeConfirmationBoxClock = presenter.$view.find('.confirmation-remove-clock');
        presenter.$removeConfirmationBoxStopwatch = presenter.$view.find('.confirmation-remove-stopwatch');
        presenter.$removeConfirmationBoxClock.attr('id', 'confirmationBox-' + model['ID']);
        presenter.$removeConfirmationBoxStopwatch.attr('id', 'confirmationBox-' + model['ID']);

        presenter.$bottomPanels = $('.bottom-panel-color, .bottom-panel-thickness, .bottom-panel-floating-image');

        presenter.config = validateModel(model);

        presenter.$panel.addClass(presenter.model['expansionDirection']);
        presenter.stateStack = new StateStack(presenter);
    };

    presenter.SHOW_PANEL = {
        '---': 'NONE',
        'Color panel': 'COLOR',
        'Thickness panel': 'THICKNESS',
        DEFAULT: '---'
    };

    function validateModel(model) {
        var validated,
            widthWhenOpened,
            widthWhenClosed;
        var hasCustomButton = model["hasCustomButton"] == 'True';
        var enableUndoRedo = ModelValidationUtils.validateBoolean(model["enableUndoRedo"]);
        var disableModuleZoom = ModelValidationUtils.validateBoolean(model["disableModuleZoom"]);

        if (!hasCustomButton) {
            presenter.$panel.children('.button.custom-script').hide();
        }

        if (model['widthWhenOpened']) {
            validated = ModelValidationUtils.validatePositiveInteger(model['widthWhenOpened']);
        } else {
            var baseWidth = 538;
            if (enableUndoRedo) baseWidth = 610;
            var width = baseWidth + hasCustomButton * 36;
            validated = getCorrectObject(width);
        }

        if (!validated.isValid) {
            return getErrorObject('E01');
        }

        widthWhenOpened = validated.value;

        if (model['Width']) {
            validated = ModelValidationUtils.validatePositiveInteger(model['Width']);
        } else {
            validated = getCorrectObject(30);
        }

        if (!validated.isValid) {
            return getErrorObject('E01');
        }

        widthWhenClosed = validated.value;

        var defaultZoom = 2;
        if (model['defaultZoom']) {
            var validated = ModelValidationUtils.validatePositiveInteger(model['defaultZoom']);
            if (validated.isValid) defaultZoom = validated.value;
        }

        return {
            'addonID': model.ID,
            'isValid': true,

            'widthWhenClosed': widthWhenClosed,
            'widthWhenOpened': widthWhenOpened,
            'panelPosition': model['Fixed Position'] == 'True' ? 'absolute' : 'fixed',

            'showForPen': ModelValidationUtils.validateOption(presenter.SHOW_PANEL, model.forPen),
            'showForMarker': ModelValidationUtils.validateOption(presenter.SHOW_PANEL, model.forMarker),
            'closedPanelDrawing': ModelValidationUtils.validateBoolean(model["Closed panel drawing"]),

            'hasCustomButton': hasCustomButton,
            'onCustomButtonSelected': model['hasCustomButton'] ? model['onCustomButtonSelected'] : null,
            'onCustomButtonDeselected': model['hasCustomButton'] ? model['onCustomButtonDeselected'] : null,
            'enableUndoRedo': enableUndoRedo,
            'disableModuleZoom': disableModuleZoom,
            'defaultZoom': defaultZoom,
            'expansionDirection': model['expansionDirection'],
            'popupsDirection': presenter.isVerticalExpansion(model['expansionDirection']) ? model['verticalPopupDirection'] : model['horizontalPopupDirection']
        };
    }

    presenter.isVerticalExpansion = function (expansionDirection) {
        return expansionDirection === 'up' || expansionDirection === 'down';
    }

    presenter.setImagePosition = function IWB_Toolbar_setImagePosition() {
        var topPosition = parseInt(/*$(window).scrollTop() + */(getCurrentImage().height() / 2)+ presenter.$panel.offset().top, 10);
        var leftPosition = parseInt((getCurrentImage().width() / 2) + presenter.$panel.position().left, 10);

        presenter.floatingImageLayer.draw();
        getCurrentImage().setAbsolutePosition(getPoint(leftPosition, topPosition));
        getCurrentMoveIcon().setAbsolutePosition(getPoint(leftPosition - 10, topPosition - 20));
        getCurrentRotateIcon().setAbsolutePosition(getPoint(leftPosition - 10, topPosition - 20));
        presenter.floatingImageLayer.draw();
    };

    presenter.preventClickActionTextAudio = function IWB_Toolbar_preventClickActionTextAudio(event) {
        if (!presenter.isZoomActive) return;

        event.stopPropagation();
        event.preventDefault();
    };

    presenter.disableTextAudioEventHandlers = function () {
        presenter.textAudioEvents = [];

        // Removes (temporally) event handlers from both words and custom controls.
        presenter.modules.find('.wrapper-addon-textaudio .textaudio-text span, .wrapper-addon-textaudio .textaudio-player div').each(function (_, element) {
            // This jQuery API is no longer available in version 1.8+ versions!
            var events = jQuery(element).data('events'),
                handlers = [];

            if (!events || !events['click']) {
                return true; // jQuery.each() continue statement
            }

            $.each(events['click'], function (_, event) {
                handlers.push(event.handler);
            });

            presenter.textAudioEvents.push({
                element: element,
                handlers: handlers
            });

            $(element).unbind('click');
            $(element).on('click', preventClickActionTextAudio);
        });
    };

    presenter.restoreTextAudioEventHandlers = function () {
        $.each(presenter.textAudioEvents, function (_, textAudioEvent) {
            var $element = $(textAudioEvent.element);

            $element.unbind('click');

            $.each(textAudioEvent.handlers, function (_, handler) {
                $element.on('click', handler);
            });
        });

        presenter.textAudioEvents = [];
    };

    presenter.penLineColor = presenter.data.penColor;
    presenter.penLineWidth = 1;

    presenter.markerLineColor = presenter.data.markerColor;
    presenter.markerLineWidth = presenter.data.markerThickness;

    presenter.penClickHandler = function IWB_Toolbar_penClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.$pagePanel.find('.iwb_tmp_canvas').hide();
        presenter.$defaultColorButton = presenter.$panel.find('.color-blue');

        presenter.changeColor(presenter.penLineColor);
        presenter.changeThickness(presenter.penLineWidth);
        if(presenter.penColorBackground){
            presenter.$panel.find('.button.color').css('background-image', presenter.penColorBackground);
        }
        if(presenter.penThicknessBackground){
            presenter.$panel.find('.button.thickness').css('background-image', presenter.penThicknessBackground);
        }

        presenter.toogleMasks();

        presenter.ctx.globalCompositeOperation = 'source-over';
        presenter.drawMode = presenter.DRAW_MODE.PEN;

        presenter.drawingLogic();

        presenter.toggleBottomPanels();
    };

    presenter.markerClickHandler = function IWB_Toolbar_markerClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.$pagePanel.find('.iwb_tmp_canvas').show();
        presenter.$defaultColorButton = presenter.$panel.find('.color-yellow');
        presenter.changeColor(presenter.markerLineColor);
        presenter.changeThickness(presenter.markerLineWidth);
        if(presenter.markerColorBackground){
            presenter.$panel.find('.button.color').css('background-image', presenter.markerColorBackground);
        }
        if(presenter.markerThicknessBackground){
            presenter.$panel.find('.button.thickness').css('background-image', presenter.markerThicknessBackground);
        }
        presenter.toogleMasks();

        presenter.markerCtx.globalCompositeOperation = 'source-over';
        presenter.drawMode = presenter.DRAW_MODE.MARKER;

        presenter.markerDrawingLogic();

        presenter.toggleBottomPanels();
        presenter.markerClicked = true;
    };

    presenter.markerCloseHandler = function IWB_Toolbar_markerCloseHandler() {
        presenter.markerLineColor = presenter.currentLineColor;
        presenter.markerLineWidth = presenter.currentLineWidth;
        presenter.markerColorBackground = presenter.$panel.find('.button.color').css('background-image');
        presenter.markerThicknessBackground = presenter.$panel.find('.button.thickness').css('background-image');
    };

    presenter.getTheWidestAndHighest = function IWB_Toolbar_getTheWidestAndHighest(elem) {
        var width = $(elem).outerWidth(),
            height = $(elem).outerHeight();

        elem.find("*").each(function () {
            if($(this).outerWidth() &gt; width){
                width = $(this).outerWidth();
            }

            if($(this).outerHeight() &gt; height){
                height = $(this).outerHeight();
            }
        });

        return {
            height: height,
            width: width
        };
    };

    presenter.preventClickAction_zoomClickHandler = function IWB_Toolbar_preventClickAction_zoomClickHandler(event) {
        event.stopPropagation();
        event.preventDefault();
    };

    presenter.zoomClickHandler = function IWB_Toolbar_zoomClickHandler(button){
        var $page = presenter.$pagePanel.find('.ic_page');
        if ($page.hasClass("zoom-cursor-zoom-in") || $page.hasClass("zoom-zoomed-in")) {
            return;
        }

        var lastEvent = null;

        presenter.panelView(button);
        presenter.$pagePanel.find('.iwb_tmp_canvas').hide();

        presenter.isZoomActive = !presenter.isZoomActive;
        presenter.$bottomPanels.hide();

        if (!presenter.isZoomActive) {
            presenter.changeCursor('default');
        } else {
            presenter.changeCursor('zoom-in');
        }
        presenter.modules = presenter.$pagePanel.find('.ic_page &gt; *:not(.iwb-toolbar-panel,.iwb-toolbar-note,.iwb-toolbar-clock,.iwb-toolbar-stopwatch,.confirmation-remove-note,.iwb-toolbar-mask)');

        if(presenter.isZoomActive){
            var pageElement = $('.ic_page');
            var defaultCoverElement = $('&lt;div class="iwb-zoom-cover iwb-default-zoom-cover"&gt;&lt;/div&gt;'),
               defMaxDimensions = presenter.getTheWidestAndHighest(pageElement);
               defaultCoverElement.css({
                  position: "absolute",
                  left: 0,
                  top: 0,
                  width: defMaxDimensions.width,
                  height: defMaxDimensions.height,
                  display: pageElement.css('display'),
                  visibility: pageElement.css('visibility')
               });
           $('.ic_page').append(defaultCoverElement);

            if (!presenter.config.disableModuleZoom) {
                presenter.modules.each(function () {
                   var coverElement = $('&lt;div class="iwb-zoom-cover"&gt;&lt;/div&gt;'),
                       maxDimensions = presenter.getTheWidestAndHighest($(this));
                   coverElement.css({
                      position: "absolute",
                      left: $(this).position().left,
                      top: $(this).position().top,
                      width: maxDimensions.width,
                      height: maxDimensions.height,
                      display: $(this).css('display'),
                      visibility: $(this).css('visibility')
                   });

                   $('.ic_page').append(coverElement);
                });
            }

            var iwbCoverElements = $(".iwb-zoom-cover");

            iwbCoverElements.on(`click ${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.UP}`, function(e) {
                e.stopPropagation();
                e.preventDefault();
            });

            presenter.modules.find('a').on('click', presenter.preventClickAction_zoomClickHandler);

            iwbCoverElements.on(EventsUtils.PointingEvents.TYPES.DOWN, function(e) {
                e.stopPropagation();
                e.preventDefault();
                lastEvent = e;
                presenter.isPointerDown = true;
                presenter.mouse.x = e.clientX;
                presenter.mouse.y = e.clientY;
            });

            iwbCoverElements.on(EventsUtils.PointingEvents.TYPES.UP, function(e) {
                e.stopPropagation();
                e.preventDefault();
                presenter.isPointerDown = false;

                if ((lastEvent.type == EventsUtils.PointingEvents.TYPES.DOWN|| lastEvent.type == EventsUtils.PointingEvents.TYPES.MOVE) &amp;&amp;
                    !$(e.currentTarget).hasClass('iwb-toolbar-panel') &amp;&amp;
                    !$(e.currentTarget).hasClass('addon_IWB_Toolbar') &amp;&amp;
                    !$(e.currentTarget).hasClass('iwb-toolbar-note') &amp;&amp;
                    !$(e.currentTarget).hasClass('iwb-toolbar-clock') &amp;&amp;
                    !$(e.currentTarget).hasClass('iwb-toolbar-stopwatch') &amp;&amp;
                    (!$(e.currentTarget).hasClass('iwb-default-zoom-cover') ||
                    (Math.abs(presenter.mouse.x - e.clientX) &lt; 20 &amp;&amp;
                    Math.abs(presenter.mouse.y - e.clientY) &lt; 20))) { // click
                        presenter.zoomSelectedModule(e);
                }
                lastEvent = e;
            });

            iwbCoverElements.on(EventsUtils.PointingEvents.TYPES.MOVE, function(e) {
                if (presenter.isPointerDown) {
                    e.stopPropagation();
                    e.preventDefault();
                    var currentScrollX = $(window).scrollLeft(),
                        currentScrollY = $(window).scrollTop(),
                        differenceX = lastEvent.clientX - e.clientX,
                        differenceY = lastEvent.clientY - e.clientY;

                    $(window).scrollLeft(currentScrollX + differenceX);
                    $(window).scrollTop(currentScrollY + differenceY);
                }

                lastEvent = e;
            });
        }else{
            $(".iwb-zoom-cover").remove();
        }

        presenter.$pagePanel.disableSelection();

        if (presenter.isZoomActive) {
            presenter.disableTextAudioEventHandlers();
        } else {
            presenter.restoreTextAudioEventHandlers();
        }

        if (presenter.areZoomEventHandlersAttached) {
            // We cannot attach multiple times the same event handlers
            return;
        }

        presenter.areZoomEventHandlersAttached = true;
    };

    presenter.zoomCloseHandler = function IWB_Toolbar_zoomCloseHandler() {
        $(".iwb-zoom-cover").remove();
    };

    presenter.eraserClickHandler = function IWB_Toolbar_eraserClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();
        presenter.panelView(button);
        presenter.$pagePanel.find('.iwb_tmp_canvas').hide();

        if (presenter.ctx) {
            presenter.ctx.globalCompositeOperation = 'destination-out';
        }
        if (presenter.markerCtx) {
            presenter.markerCtx.globalCompositeOperation = 'destination-out';
        }

        presenter.$penMask.css('pointer-events', 'auto');
        presenter.$penMask.css('touch-action', 'none');
        presenter.$markerMask.css('pointer-events', 'auto');
        presenter.$markerMask.css('touch-action', 'none');

        presenter.changeColor('rgba(0, 0, 0, 1)');
        presenter.changeThickness(presenter.data.eraserThickness);
        presenter.drawMode = presenter.DRAW_MODE.ERASER;
        presenter.drawingLogic();
        presenter.toogleMasks();
    };

    presenter.eraserUnClickHandler = function IWB_Toolbar_eraserUnClickHandler(button) {
        presenter.panelView(button);

        presenter.$penMask.css('pointer-events', 'none');
        presenter.$penMask.css('touch-action', 'auto');
        presenter.$markerMask.css('pointer-events', 'none');
        presenter.$markerMask.css('touch-action', 'auto');
    };

    presenter.hideAreaClickHandler = function IWB_Toolbar_hideAreaClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.$pagePanel.find('.iwb_tmp_canvas').hide();
        presenter.toogleMasks();
        presenter.drawAreaLogic(true);

        presenter.drawMode = presenter.DRAW_MODE.HIDE_AREA;
        presenter.$defaultColorButton = presenter.$panel.find('.color-black');
        presenter.changeColor('#000');
    };

    presenter.standAreaClickHandler = function IWB_Toolbar_standAreaClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.$pagePanel.find('.iwb_tmp_canvas').hide();
        presenter.toogleMasks();
        presenter.drawAreaLogic(false);

        presenter.drawMode = presenter.DRAW_MODE.STAND_AREA;
        presenter.$defaultColorButton = presenter.$panel.find('.color-black');
        presenter.changeColor('#000');
    };

    presenter.resetClickHandler = function IWB_Toolbar_resetClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.selectingCtx.clearRect(0, 0, presenter.$selectingMask.width(), presenter.$selectingMask.height());
        presenter.ctx.clearRect(0, 0, presenter.$penMask.width(), presenter.$penMask.height());
        presenter.markerCtx.clearRect(0, 0, presenter.$markerMask.width(), presenter.$markerMask.height());

        presenter.areas = [];
        presenter.drawMode = presenter.DRAW_MODE.NONE;

        $(".iwb-zoom-cover").remove();

        presenter._reset(true, false, false, false, false, false);

        if (presenter.isCustomButtonActive()) {
            var customScriptButton = presenter.$panel.find('.' + CUSTOM_BUTTON_NAME)[0];
            presenter.customScriptClickHandler(customScriptButton);
        }

        presenter.penDataURL = null;
        presenter.markerDataUrl = null;

        presenter.pushStateToStack();
    };

    presenter.resetOneClickHandler = function IWB_Toolbar_resetOneClickHandler(button) {
        presenter.restoreLastState();
    };

    presenter.redoOneClickHandler = function IWB_Toolbar_redoOneClickHandler(button) {
        presenter.redoStateFromArray();
    };

    presenter.noteClickHandler = function IWB_Toolbar_noteClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);

        var note = presenter.createNote();
        presenter.noteObjects.push(note);

        var $noteView = note.getView();
        presenter.$pagePanel.find('.ic_page').append($noteView);

        presenter.$pagePanel.find('.iwb-toolbar-note').click(function(e) {
            e.stopPropagation();
        });
        presenter.$pagePanel.find('.note').on(EventsUtils.PointingEvents.TYPES.DOWN, function() {
            presenter.$pagePanel.find('.note').addClass('clicked');
        });
        presenter.$pagePanel.find('.note').on(EventsUtils.PointingEvents.TYPES.UP, function() {
            presenter.$pagePanel.find('.note').removeClass('clicked');
        });

        presenter.pushStateToStack();
    };

    presenter.customScriptCreateEventData = function() {
        return {
            source : presenter.config.addonID,
            value : presenter.isCustomButtonActive() ? '1' : '0',
        };
    };

    presenter.customScriptSendEventData = function () {
        var eventData = presenter.customScriptCreateEventData();
        if (presenter.playerController !== null) {
            presenter.playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.customScriptClickHandler = function IWB_Toolbar_customScriptClickHandler(button) {
        if (presenter.config.hasCustomButton) {
            presenter.isZoomActive = false;
            presenter.restoreTextAudioEventHandlers();

            presenter.panelView(button);
            if (!presenter.isCommandSourceToDeselectCustomButton) {
                presenter.runCustomScript();
                presenter.customScriptSendEventData();
            }
            presenter.isCommandSourceToDeselectCustomButton = false;
        }
    };

    presenter.floatingImageClickHandler = function IWB_Toolbar_floatingImageClickHandler(button) {
        if (isJQueryObject(button) &amp;&amp; !isFloatingImageBtnVisible()) {
            return;
        }

        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        $.when.apply($, presenter.allImagesLoadedPromises).then(function() {
            var display = presenter.$pagePanel.find('.floating-image-mask').css('display');
            if (display === 'none') {
                presenter.$floatingImageMask.show();
                presenter.$pagePanel.find('.bottom-panel-floating-image').show();
                presenter.$pagePanel.find('.bottom-panel-floating-image').attr('isHidden', '1');
            } else {
                presenter.$floatingImageMask.hide();
                presenter.$pagePanel.find('.bottom-panel-floating-image').hide();
                presenter.$pagePanel.find('.floating-image').removeClass('clicked');
                presenter.$pagePanel.find('.bottom-panel-floating-image').attr('isHidden', '0');
            }
            presenter.setImagePosition();
        });
    };

    function isJQueryObject(element) {
        return !!element[0];
    }

    function isFloatingImageBtnVisible() {
        return $('.floating-image').css('display') !== 'none';
    }

    presenter.clockClickHandler = function IWB_Toolbar_clockClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.$pagePanel.find('.clock').on(EventsUtils.PointingEvents.TYPES.DOWN, function() {
            presenter.$pagePanel.find('.clock').addClass('clicked');
        });
        presenter.$pagePanel.find('.clock').on(EventsUtils.PointingEvents.TYPES.UP, function() {
            presenter.$pagePanel.find('.clock').removeClass('clicked');
        });
        presenter.createClock();
        presenter.pushStateToStack();
    };

    presenter.stopwatchClickHandler = function IWB_Toolbar_stopwatchClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.$pagePanel.find('.stopwatch').on(EventsUtils.PointingEvents.TYPES.DOWN, function() {
            presenter.$pagePanel.find('.stopwatch').addClass('clicked');
        });
        presenter.$pagePanel.find('.stopwatch').on(EventsUtils.PointingEvents.TYPES.UP, function() {
            presenter.$pagePanel.find('.stopwatch').removeClass('clicked');
        });
        presenter.createStopwatch();
        presenter.pushStateToStack();
    };

    presenter.closeClickHandler = function IWB_Toolbar_closeClickHandler(button) {
        if(presenter.shouldSaveColor == 'pen' || presenter.shouldSaveColor == 'stand-area' || presenter.shouldSaveColor == 'hide-area'){
            presenter.closePenColor = presenter.currentLineColor;
            presenter.closePenThickness = presenter.currentLineWidth;

            presenter.isCloseColor = true;
        }else if(presenter.shouldSaveColor == 'marker'){
            presenter.closePenColor = presenter.currentLineColor;
            presenter.closePenThickness = presenter.currentMarkerThickness;

            presenter.isCloseColor = true;
        }
        else{
            presenter.isCloseColor = false;
        }
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        presenter.$panel.find('.clicked').removeClass('clicked');
        presenter.closePanel(true);
        presenter.isPanelOpened = false;
        if(presenter.activeButton != 'open'){
            presenter.activeFunction = presenter.activeButton;
        }
    };

    presenter.openClickHandler = function IWB_Toolbar_openClickHandler(button){
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
        if (!presenter.isPanelOpened) {
            presenter.openPanel(true);
        }

        if (presenter.customButtonActive) {
            var button = presenter.$pagePanel.find('.' + CUSTOM_BUTTON_NAME);
            button.addClass('clicked');
        }

        if(presenter.activeFunction){
            if(['clock', 'stopwatch', 'note', 'reset', 'reset-one', 'redo-one'].indexOf(presenter.activeFunction) == -1){
                if(!presenter.recklick){
                    presenter.functionButton = presenter.$pagePanel.find('.'+presenter.activeFunction);
                    presenter.buttonsLogic[presenter.activeFunction].onOpen(presenter.functionButton);
                }
                presenter.isRecklicked = false;
            }
        }

        presenter.isPanelOpened = true;

        if(presenter.isSavedState){
            if(presenter.isCloseColor){
                presenter.changeColor(presenter.closePenColor, presenter.buttonColor);
                presenter.changeThickness(presenter.closePenThickness, presenter.buttonThickness);
            }
            presenter.isSavedState = false;
        }else{
            if(presenter.isCloseColor){
                presenter.changeColor(presenter.closePenColor, presenter.buttonColor);
                presenter.changeThickness(presenter.closePenThickness, presenter.buttonThickness);
            }else{
                presenter.changeColor('#000', presenter.$bottomPanels.find('.color-black'));
                presenter.changeThickness(1, presenter.$bottomPanels.find('.thickness-1'));
            }
        }
    };

    presenter.panelView = function IWB_Toolbar_panelView (button) {
        var shouldClosePanels = shouldClosePanelsOnReset(button);

        presenter._reset(shouldClosePanels, false, shouldHideDrawingMasks(button), shouldHideSelectingMasks(button), shouldHideFloatingImage(button), false);
        if (!$(button).hasClass('open') &amp;&amp; !$(button).hasClass('close')) {
            if ($(button).hasClass('clicked')) {
                $(button).removeClass('clicked');
            } else {
                presenter.$panel.find('.clicked').not('.custom-script').removeClass('clicked');
                $(button).addClass('clicked');
            }
        }
        if (presenter.isSupportCSSPointerEvents()) {
            if (!$(button).hasClass('pen') &amp;&amp; !$(button).hasClass('marker') &amp;&amp; !$(button).hasClass('eraser')){
                if(!presenter.config.closedPanelDrawing) {
                    presenter.$penMask.css('pointer-events', 'none');
                    presenter.$markerMask.css('pointer-events', 'none');
                    presenter.$penMask.css('touch-action', 'auto');
                    presenter.$markerMask.css('touch-action', 'auto');
                }
            } else {
                presenter.$penMask.css('pointer-events', 'auto');
                presenter.$markerMask.css('pointer-events', 'auto');
                presenter.$penMask.css('touch-action', 'none');
                presenter.$markerMask.css('touch-action', 'none');
            }
        }
        changeCurrentFloatingImage(presenter.currentFloatingImageIndex);
    };

    presenter.colorClickHandler = function IWB_Toolbar_colorClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        if (presenter.areDrawingButtonsActive()) {
            presenter.$panel.find('.button.clicked-lighter').removeClass('clicked-lighter');
            $(button).toggleClass('clicked-lighter');
        }
    };

    presenter.thicknessClickHandler = function IWB_Toolbar_thicknessClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        if (presenter.areDrawingButtonsActive()) {
            presenter.$panel.find('.button.clicked-lighter').removeClass('clicked-lighter');
            $(button).toggleClass('clicked-lighter');
        }
    };

    presenter.defaultClickHandler = function IWB_Toolbar_defaultClickHandler(button) {
        presenter.isZoomActive = false;
        presenter.restoreTextAudioEventHandlers();

        presenter.panelView(button);
    };

    presenter.penUnclickHandler = function IWB_Toolbar_penUnclickHandler (button) {
        presenter._reset(true, false, 1, true, true, false);
        if ($(button).hasClass('clicked')) {
            $(button).removeClass('clicked');
        } else {
            presenter.$panel.find('.clicked').removeClass('clicked');
            $(button).addClass('clicked');
        }

        presenter.$penMask.css('pointer-events', 'none');
        presenter.$markerMask.css('pointer-events', 'none');
        presenter.$penMask.css('touch-action', 'auto');
        presenter.$markerMask.css('touch-action', 'auto');

        presenter.penLineColor = presenter.currentLineColor;
        presenter.penLineWidth = presenter.currentLineWidth;
        presenter.colorBackground = presenter.$panel.find('.button.color').css('background-image');
        presenter.penThicknessBackground = presenter.$panel.find('.button.thickness').css('background-image');

        presenter.penClicked = false;
    };

    presenter.markerUnclickHandler = function IWB_Toolbar_markerUnclickHandler(button) {
        presenter._reset(true, false, 1, true, true, false);
        if ($(button).hasClass('clicked')) {
            $(button).removeClass('clicked');
        } else {
            presenter.$panel.find('.clicked').removeClass('clicked');
            $(button).addClass('clicked');
        }

        presenter.$penMask.css('pointer-events', 'none');
        presenter.$markerMask.css('pointer-events', 'none');
        presenter.$penMask.css('touch-action', 'auto');
        presenter.$markerMask.css('touch-action', 'auto');

        presenter.markerLineColor = presenter.currentLineColor;
        presenter.markerLineWidth = presenter.currentLineWidth;
        presenter.markerColorBackground = presenter.$panel.find('.button.color').css('background-image');
        presenter.markerThicknessBackground = presenter.$panel.find('.button.thickness').css('background-image');
        presenter.markerClicked = false;
    };

    presenter.penCloseHandler = function IWB_Toolbar_penCloseHandler() {
        presenter.penLineColor = presenter.currentLineColor;
        presenter.penLineWidth = presenter.currentLineWidth;
        presenter.penColorBackground = presenter.$panel.find('.button.color').css('background-image');
        presenter.penThicknessBackground = presenter.$panel.find('.button.thickness').css('background-image');
    };

    presenter.buttonsLogic = {
        'pen' : {
            'onOpen': presenter.penClickHandler,
            'onClose': presenter.penCloseHandler,
            'onReclicked': presenter.penUnclickHandler
        },
        'marker' : {
            'onOpen': presenter.markerClickHandler,
            'onClose': presenter.markerCloseHandler,
            'onReclicked': presenter.markerUnclickHandler
        },
        'default' : {
            'onOpen': presenter.defaultClickHandler,
            'onReclicked': presenter.defaultClickHandler
        },
        'color' : {
            'onOpen': presenter.colorClickHandler,
            'onReclicked': presenter.colorClickHandler
        },
        'thickness' : {
            'onOpen': presenter.thicknessClickHandler,
            'onReclicked': presenter.thicknessClickHandler
        },
        'zoom' : {
            'onOpen': presenter.zoomClickHandler,
            'onReclicked': presenter.zoomClickHandler,
            'onClose': presenter.zoomCloseHandler
        },
        'eraser' : {
            'onOpen': presenter.eraserClickHandler,
            'onReclicked': presenter.eraserUnClickHandler
        },
        'hide-area' : {
            'onOpen': presenter.hideAreaClickHandler,
            'onReclicked': presenter.hideAreaClickHandler
        },
        'stand-area' : {
            'onOpen': presenter.standAreaClickHandler,
            'onClose': '',
            'onReclicked': presenter.standAreaClickHandler
        },
        'reset' : {
            'onOpen': presenter.resetClickHandler,
            'onReclicked': presenter.resetClickHandler
        },
        'reset-one' : {
            'onOpen': presenter.resetOneClickHandler,
            'onClose': '',
            'onReclicked': presenter.resetOneClickHandler
        },
        'redo-one' : {
            'onOpen': presenter.redoOneClickHandler,
            'onReclicked': presenter.redoOneClickHandler
        },
        'note' : {
            'onOpen': presenter.noteClickHandler,
            'onReclicked': presenter.noteClickHandler
        },
        'custom-script' : {
            'onOpen': presenter.customScriptClickHandler,
            'onReclicked': presenter.customScriptClickHandler
        },
        'floating-image' : {
            'onOpen': presenter.floatingImageClickHandler,
            'onReclicked': presenter.floatingImageClickHandler
        },
        'clock' : {
            'onOpen': presenter.clockClickHandler,
            'onReclicked': presenter.clockClickHandler
        },
        'stopwatch' : {
            'onOpen': presenter.stopwatchClickHandler,
            'onReclicked': presenter.stopwatchClickHandler
        },
        'close' : {
            'onOpen': presenter.closeClickHandler
        },
        'open' : {
            'onOpen': presenter.openClickHandler
        }
    };

    function isReclickable(button) {
        var $button = $(button);
        return !$button.hasClass('open')
        &amp;&amp; !$button.hasClass('close')
        &amp;&amp; !$button.hasClass('reset')
        &amp;&amp; !$button.hasClass('reset-one')
        &amp;&amp; !$button.hasClass('redo-one');
    }

    function clickHandlers (button) {
        presenter._iwb_buttons.push($(button));
        var buttonName = $(button).data("name"),
            sameButton = presenter.$pagePanel.find('.clicked').data("name") == $(button).data("name");

        if (buttonName == CUSTOM_BUTTON_NAME) {
            if (!presenter.customButtonActive) {
                presenter.customButtonActive = true;
                presenter.buttonsLogic[$(button).attr("data-name")].onOpen(button);
            } else {
                presenter.customButtonActive = false;
                presenter.buttonsLogic[$(button).attr("data-name")].onReclicked(button);
            }
            return;
        }

        if(presenter.activeButton != '' &amp;&amp; presenter.buttonsLogic[presenter.activeButton].onClose){
            presenter.buttonsLogic[presenter.activeButton].onClose(button);
        }

        if(presenter.activeButton == 'open' &amp;&amp; presenter.buttonClicked &amp;&amp; !presenter.recklick &amp;&amp; sameButton){
            presenter.activeButton = buttonName;
        }

        if(buttonName == presenter.activeButton){
            if(!presenter.isRecklicked){
                if(presenter.buttonsLogic[presenter.activeButton].onReclicked){
                    presenter.buttonsLogic[presenter.activeButton].onReclicked(button);
                }
                presenter.isRecklicked = true;
                if(isReclickable(button)) presenter.recklick = true;
            }else{
                presenter.buttonsLogic[$(button).attr("data-name")].onOpen(button);
                presenter.isRecklicked = false;
                if(isReclickable(button)) presenter.recklick = false;
            }
        }else{
            presenter.buttonsLogic[$(button).attr("data-name")].onOpen(button);
            presenter.isRecklicked = false;
            if(isReclickable(button)) presenter.recklick = false;
        }

        if(!$(button).hasClass('color') &amp;&amp; !$(button).hasClass('thickness')){
            presenter.activeButton = $(button).attr("data-name");
        }
    }

    presenter.deselectCustomButton = function () {
        if (presenter.config.hasCustomButton &amp;&amp; presenter.customButtonActive) {
            if (isPanelOpened()) {
                presenter.isCommandSourceToDeselectCustomButton = true;
                var button = presenter.$pagePanel.find('.button.custom-script.clicked')[0];
                clickHandlers(button);
            } else {
                presenter.customButtonActive = false;
            }
        }
    }

    presenter.addEventHandlers = function IWB_Toolbar_addEventHandlers() {
        presenter.$pagePanel.find('.iwb-toolbar-mask').click(function(e) {
            e.stopPropagation();
        });

        presenter.$pagePanel.find('.iwb-toolbar-note').click(function(e) {
            e.stopPropagation();
        });

        presenter.$panel.click(function(e) {
            e.stopPropagation();
        });

        presenter.$pagePanel.find('.button').on(getTouchStartOrMouseDownEventName(), function(e) {
            if($(this).hasClass('yes-button') || $(this).hasClass('no-button')){
                return;
            }

            e.stopPropagation();
            e.preventDefault();
            if(presenter.isSavedState){
                presenter.buttonClicked = true;
            }
            clickHandlers(this);
            if (isDependingOnDrawing(this) &amp;&amp; presenter.areDrawingButtonsActive() || isFloatingImageButton(this)) {
                openBottomPanel(this);
            }
            if ($(this).hasClass('reset') || $(this).hasClass('reset-one') || $(this).hasClass('redo-one')) {
                $(this).removeClass('clicked');
            }

            if(!$(this).hasClass('open') &amp;&amp; !$(this).hasClass('note') &amp;&amp; !$(this).hasClass('stopwatch') &amp;&amp; !$(this).hasClass('clock') &amp;&amp; !$(this).hasClass('close')){
                presenter.buttonClicked = true;
            }

            if($(this).hasClass('reset') || $(this).hasClass('reset-one') || $(this).hasClass('redo-one') || $(this).hasClass('default') || $(this).hasClass('note') || $(this).hasClass('stopwatch') || $(this).hasClass('clock')){
                presenter.buttonClicked = false;
            }

            var btnName = $(this).data("name");

            if(btnName != 'open' &amp;&amp; btnName != 'close' &amp;&amp; btnName != 'color' &amp;&amp; btnName != 'thickness'){
                presenter.shouldSaveColor = btnName;
            }
        });

        presenter.$pagePanel.find('.button-drawing-details').on(getTouchStartOrMouseDownEventName(), function(e) {
            e.stopPropagation();
            e.preventDefault();
            changeBottomButtonState(this);

            changeDrawingType(this);
        });

        presenter.$pagePanel.find('.button-floating-image').on(getTouchStartOrMouseDownEventName(), function(e) {
            e.stopPropagation();
            e.preventDefault();
            changeBottomButtonState(this);

            changeCurrentFloatingImage(parseInt($(this).attr('index'), 10));
            presenter.setImagePosition();
        });
    };

    function applyOnDblClickHandler() {
        if (presenter.floatingImageMode == presenter.FLOATING_IMAGE_MODE.MOVE) {
            presenter.floatingImageMode = presenter.FLOATING_IMAGE_MODE.ROTATE;
            getCurrentMoveIcon().visible(false);
            getCurrentRotateIcon().visible(true);
            getCurrentGroup().draggable(false);
            presenter.floatingImageLayer.draw();
        } else {
            presenter.floatingImageMode = presenter.FLOATING_IMAGE_MODE.MOVE;
            getCurrentMoveIcon().visible(true);
            getCurrentRotateIcon().visible(false);
            getCurrentGroup().draggable(true);
            presenter.floatingImageLayer.draw();
        }
    }

    presenter.isLeft = function(center, startPos, currentPos) {
        return ((startPos.x - center.x)*(currentPos.y - center.y) - (startPos.y - center.y)*(currentPos.x - center.x)) &gt;= 0;
    };

    function Vector(imageCenterPosition, mousePosition) {
        this.x = imageCenterPosition.x - mousePosition.x;
        this.y = imageCenterPosition.y - mousePosition.y;
        this.length = Math.sqrt(this.x * this.x + this.y * this.y);
    }

    presenter._calculateVectorsAngle = function (v1, v2) {
        var angleArg = (v1.x * v2.x + v1.y * v2.y) / (v1.length * v2.length);
    
        if (v1.length === 0 || v2.length === 0 || v1.length === v2.length || -1 &gt; angleArg || angleArg &gt;= 1) {
            /*
            Comments to loop conditions:
            - can not be divided by zero  [v1.length === 0 || v2.length === 0]
            - arccos(1) = 0  [v1.length === v2.length]
            - range of value: -1 &lt;= cos(x) &lt; 1  [-1 &gt; angleArg || angleArg &gt;= 1]
            */
            return {isCorrect: false};
        }
        
        return {isCorrect: true, angleValue: Math.acos(angleArg)};
    };

    function changeCurrentFloatingImage(index) {
        $.when.apply($, presenter.allImagesLoadedPromises).then(function() {
            presenter.$panel.find('.button-floating-image-' + (index + 1)).addClass('clicked-lighter');
            getCurrentGroup().visible(false);
            presenter.currentFloatingImageIndex = index;
            getCurrentGroup().visible(true);
            presenter.floatingImageLayer.draw();

            var isPointerDown = false,
                startingVector = null;

            function rotateActionStartHandler() {
                if (presenter.floatingImageMode == presenter.FLOATING_IMAGE_MODE.ROTATE) {
                    isPointerDown = true;
                    var imageCenter = {
                        x: (getCurrentImage().getAbsolutePosition().x),
                        y: (getCurrentImage().getAbsolutePosition().y)
                    };

                    startingVector = new Vector(imageCenter, presenter.floatingImageStage.getPointerPosition());
                }
            }

            presenter.$floatingImageMask.off(`${EventsUtils.PointingEvents.TYPES.DOWN} touchstart ${EventsUtils.PointingEvents.TYPES.UP} touchend touchmove ${EventsUtils.PointingEvents.TYPES.MOVE}`);
            presenter.$floatingImageMask.on(EventsUtils.PointingEvents.TYPES.DOWN, rotateActionStartHandler);
            presenter.$floatingImageMask.on('touchstart', rotateActionStartHandler);

            function rotateActionEndHandler() {
                if (presenter.floatingImageMode == presenter.FLOATING_IMAGE_MODE.ROTATE) {
                    isPointerDown = false;
                }
            }

            presenter.$floatingImageMask.on(EventsUtils.PointingEvents.TYPES.UP, rotateActionEndHandler);
            presenter.$floatingImageMask.on('touchend', rotateActionEndHandler);

            var previousPosition = null;

            function rotateActionMoveHandler() {
                var currentPosition = presenter.floatingImageStage.getPointerPosition();

                if (isPointerDown &amp;&amp; presenter.floatingImageMode == presenter.FLOATING_IMAGE_MODE.ROTATE &amp;&amp; previousPosition) {
                    var imageCenter = {
                        x: (getCurrentImage().getAbsolutePosition().x),
                        y: (getCurrentImage().getAbsolutePosition().y)
                    };

                    var currentVector = new Vector(imageCenter, presenter.floatingImageStage.getPointerPosition());
                    var angle = presenter._calculateVectorsAngle(startingVector, currentVector);
                    var isLeft = presenter.isLeft(imageCenter, previousPosition, currentPosition);
                    
                    if (angle.isCorrect == false){
                        getCurrentImage();
                        presenter.floatingImageLayer.draw();
                    } else {
                        getCurrentImage().rotate(isLeft ? angle.angleValue : -angle.angleValue);
                        presenter.floatingImageLayer.draw();
                    }
                }

                previousPosition = currentPosition;
            }

            presenter.$floatingImageMask.on(EventsUtils.PointingEvents.TYPES.MOVE, rotateActionMoveHandler);
            presenter.$floatingImageMask.on('touchmove', rotateActionMoveHandler);
        });
    }

    presenter.addFloatingImages = function IWB_Toolbar_addFloatingImages (model) {
        var $mask = $('&lt;div class="iwb-toolbar-mask floating-image-mask"&gt;&lt;/div&gt;');
        presenter.$pagePanel.find('.ic_page').append($mask);
        $mask.hide();

        var stage = new Kinetic.Stage({
            container: $mask[0],
            visible: true,
            width: $mask.width(),
            height: $mask.height()
        });

        var layer = new Kinetic.Layer();

        presenter.$floatingImageMask = $mask;
        presenter.floatingImageLayer = layer;
        presenter.floatingImageStage = stage;
        presenter.allImagesLoadedPromises = [];

        for (var index = 0; index &lt; 3; index++) {
            presenter.loadDeferredFloatingImage(index, model, layer, stage);
        }
    };

    presenter.loadDeferredFloatingImage = function(index, model, layer, stage) {
        var imageObj = new Image();
        var deferredImage = new $.Deferred();
        presenter._kinetic.imageObj.push(imageObj);
        $(imageObj).load(function() {
            var group = new Kinetic.Group({
                draggable: true,
                visible: index == presenter.currentFloatingImageIndex
            });

            var image = new Kinetic.Image({
                x: imageObj.width / 2,
                y: $(window).scrollTop() + (imageObj.height / 2)+ presenter.$panel.offset().top,
                image: imageObj,
                width: imageObj.width,
                height: imageObj.height,
                offset: { x: imageObj.width / 2, y: imageObj.height / 2 }
            });

            group.on('dblclick', function() {
                applyOnDblClickHandler();
            });

            presenter._kinetic.images.push(image);

            applyDoubleTapHandler(group, applyOnDblClickHandler);

            var imageMoveObj = new Image();
            $(imageMoveObj).load(function() {
                var moveIcon = new Kinetic.Image({
                    x: (imageObj.width / 2) - 16, // -16, czyli połowa szerokości obrazka
                    y: (imageObj.height / 2) - 16 + presenter.$panel.offset().top, // -16, czyli połowa wysokości obrazka
                    image: imageMoveObj,
                    opacity: 0.4
                });

                presenter._kinetic.moveIcon.push(moveIcon);
                var imageRotateObj = new Image();
                $(imageRotateObj).load(function() {
                    var rotateIcon = new Kinetic.Image({
                        x: (imageObj.width / 2) - 16, // -16, czyli połowa szerokości obrazka
                        y: (imageObj.height / 2) - 16 + presenter.$panel.offset().top, // -16, czyli połowa wysokości obrazka
                        image: imageRotateObj,
                        visible: false,
                        opacity: 0.4
                    });

                    group.add(image);
                    group.add(moveIcon);
                    group.add(rotateIcon);
                    layer.add(group);
                    stage.add(layer);
                    presenter.floatingImageGroups[index] = group;
                    presenter._kinetic.rotateIcon.push(rotateIcon);

                    deferredImage.resolve();
                });
                imageRotateObj.src = DOMOperationsUtils.getResourceFullPath(presenter.playerController, 'addons/resources/it_rotate.png');
                presenter._kinetic.rotateObj.push(imageRotateObj);

            });
            imageMoveObj.src = DOMOperationsUtils.getResourceFullPath(presenter.playerController, 'addons/resources/it_move.png');

            presenter._kinetic.moveObj.push(imageMoveObj);
        });


        presenter.allImagesLoadedPromises.push(deferredImage.promise());
        if (model['floatingImages'] &amp;&amp; model['floatingImages'][index] &amp;&amp; model['floatingImages'][index]['Image'].length &gt; 0) {
            imageObj.src = model['floatingImages'][index]['Image'];
        } else {
            imageObj.src = DOMOperationsUtils.getResourceFullPath(presenter.playerController, 'addons/resources/' + presenter.DEFAULT_FLOATING_IMAGE[index]);
        }
    };

    presenter.getZoomHeightScale = function () {
        var initialSize = presenter.zoomConfiguration.initialWindowHeight - presenter.zoomConfiguration.initialNotScaledOffset;
        var newSize = window.iframeSize.windowInnerHeight - presenter.zoomConfiguration.initialNotScaledOffset;

        var zoomHeightScale = newSize / initialSize;

        zoomHeightScale = zoomHeightScale === 0 || isNaN(zoomHeightScale) || !isFinite(zoomHeightScale) ? 1 : zoomHeightScale;

        return zoomHeightScale
    };

    presenter.isOnScreen = function (element, windowElement) {
        if (element == null || element.length === 0) return false;
        var zoomHeightScale = presenter.getZoomHeightScale();
        var topWindow = $(windowElement.parent.document);
        var coords = {
            top: topWindow.scrollTop(),
            left: topWindow.scrollLeft(),
            right: topWindow.scrollLeft() + topWindow.width(),
            bottom: topWindow.scrollTop() + topWindow.height()
        };

        var bounds = element.offset();
        bounds.right = bounds.left + element.outerWidth();
        bounds.bottom = bounds.top + element.outerHeight();

        bounds.bottom /= zoomHeightScale;

        return !(coords.right &lt; bounds.left || coords.left &gt; bounds.right || coords.bottom &lt; bounds.top || coords.top &gt; bounds.bottom);
    };

    function addScrollHandler() {
        if (!presenter.playerController || presenter.playerController.isPlayerInCrossDomain()) return;
        var $defaultScrollElement = $(window.parent.document);
        var $mCourserScrollElement = $defaultScrollElement.find('#lesson-view &gt; div &gt; div');
        var $mAuthorMobileScrollElement = $(window);
        var $mCourserMobileScrollElement = $("#content-view");
        var scrollElements = [$defaultScrollElement, $mCourserScrollElement, $mAuthorMobileScrollElement, $mCourserMobileScrollElement];
        try {
            for (var i = 0; i &lt; scrollElements.length; i++) {
                scrollElements[i].scroll(IWB_Toolbar_scrollHandler);
            }
        } catch(e) {}
    }

    var difference = 0,
        lastScrollTop = 0,
        panelTop = 0;
    function IWB_Toolbar_scrollHandler() {
        if (presenter.isOnScreen(presenter.$view.parent(), window)) {
            var containerHeight = presenter.$pagePanel.outerHeight(true),
                scrollTop = $(this).scrollTop(),
                baseHeightScale = presenter.playerController.getScaleInformation().scaleY,
                min = presenter.$pagePanel.offset().top / baseHeightScale,
                headerHeight = $('.ic_header').outerHeight(true) - 20,
                max = (containerHeight + headerHeight) / baseHeightScale,
                zoomHeightScale = presenter.getZoomHeightScale();
            difference = scrollTop - lastScrollTop;
            panelTop = parseInt(presenter.$panel.css('top'), 10) + difference * zoomHeightScale / baseHeightScale;
            lastScrollTop = scrollTop;

            if (panelTop &amp;&amp; (panelTop) &gt; min &amp;&amp; (panelTop) &lt; max) {
                presenter.$panel.css({
                    'top': (panelTop) + 'px'
                });
            } else if (panelTop &amp;&amp; (panelTop) &gt;= max) {
                presenter.$panel.css({
                    'top': (containerHeight - presenter.$panel.outerHeight(true) + min) + 'px'
                });
            } else if (panelTop &amp;&amp; (panelTop) &lt;= min) {
                presenter.$panel.css({
                    'top': min + 'px'
                });
            }
        }
    }

    function drawSketch() {
        var sketch = presenter.$view.parent().find('.selecting');
        var canvas = sketch.find('canvas');
        canvas.width = sketch.width;
        canvas.height = sketch.height;

        var iwb_tmp_canvas = document.createElement('canvas');
        var tmp_ctx = iwb_tmp_canvas.getContext('2d');
        iwb_tmp_canvas.id = 'iwb_tmp_canvas';
        iwb_tmp_canvas.width = canvas.width();
        iwb_tmp_canvas.height = canvas.height();

        var $tmpCanvas = $('#iwb_tmp_canvas');
        sketch.css("touch-action", "none");

        if ($.contains(document, $tmpCanvas[0])) {
            $tmpCanvas.remove();
        }

        presenter.$view.parent().find('.selecting').append(iwb_tmp_canvas);

        var mouse = getPoint(0, 0);
        var start_mouse = getPoint(0, 0);

        /* Mouse Capturing Work */
        if( /Android|X11|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
            iwb_tmp_canvas.addEventListener('touchmove', function(e) {
                e.stopPropagation();
                e.preventDefault();

                mouse.x = typeof e.offsetX !== 'undefined' ? e.offsetX : e.layerX;
                mouse.y = typeof e.offsetY !== 'undefined' ? e.offsetY : e.layerY;
            }, false);

            iwb_tmp_canvas.addEventListener('touchstart', function(e) {
                iwb_tmp_canvas.addEventListener('touchmove', onPaint, false);

                e.stopPropagation();
                e.preventDefault();
                mouse.x = typeof e.offsetX !== 'undefined' ? e.offsetX : e.layerX;
                mouse.y = typeof e.offsetY !== 'undefined' ? e.offsetY : e.layerY;

                start_mouse.x = mouse.x;
                start_mouse.y = mouse.y;

                onPaint();
            }, false);

            iwb_tmp_canvas.addEventListener('touchend', function(e) {
                iwb_tmp_canvas.removeEventListener('touchmove', onPaint, false);
                e.stopPropagation();
                e.preventDefault();
                // Writing down to real canvas now
                // ctx.drawImage(iwb_tmp_canvas, 0, 0);
                // Clearing tmp canvas
                tmp_ctx.clearRect(0, 0, iwb_tmp_canvas.width, iwb_tmp_canvas.height);
            }, false);
        }else{
            iwb_tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, function(e) {
                e.stopPropagation();
                e.preventDefault();
                mouse.x = typeof e.offsetX !== 'undefined' ?  e.offsetX : e.layerX;
                mouse.y = typeof e.offsetY !== 'undefined' ? e.offsetY : e.layerY;
            }, false);

            iwb_tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.DOWN, function(e) {
                e.stopPropagation();
                e.preventDefault();
                iwb_tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, onPaint, false);

                mouse.x = typeof e.offsetX !== 'undefined' ? e.offsetX : e.layerX;
                mouse.y = typeof e.offsetY !== 'undefined' ? e.offsetY : e.layerY;

                start_mouse.x = mouse.x;
                start_mouse.y = mouse.y;
                onPaint();
            }, false);

            iwb_tmp_canvas.addEventListener(EventsUtils.PointingEvents.TYPES.UP, function(e) {
                e.stopPropagation();
                e.preventDefault();
                iwb_tmp_canvas.removeEventListener(EventsUtils.PointingEvents.TYPES.MOVE, onPaint, false);
                // Writing down to real canvas now
                // ctx.drawImage(iwb_tmp_canvas, 0, 0);
                // Clearing tmp canvas
                tmp_ctx.clearRect(0, 0, iwb_tmp_canvas.width, iwb_tmp_canvas.height);
            }, false);
        }

        /* Drawing on Paint App */
        tmp_ctx.lineWidth = 1;
        tmp_ctx.lineJoin = 'round';
        tmp_ctx.lineCap = 'round';
        tmp_ctx.strokeStyle = 'black';
        tmp_ctx.fillStyle = 'black';

        var onPaint = function() {
            // Tmp canvas is always cleared up before drawing.
            tmp_ctx.clearRect(0, 0, iwb_tmp_canvas.width, iwb_tmp_canvas.height);

            var x = Math.min(mouse.x, start_mouse.x);
            var y = Math.min(mouse.y, start_mouse.y);
            var width = Math.abs(mouse.x - start_mouse.x);
            var height = Math.abs(mouse.y - start_mouse.y);
            tmp_ctx.strokeRect(x, y, width, height);
        };
    }

    presenter.drawAreaLogic = function IWB_Toolbar_drawAreaLogic(isHide) {
        drawSketch();

        presenter.drawAreaLogic_touchEndCallback = function (event) {
            var pos = getCursorPosition(event.originalEvent);

            presenter.stopSelection = getPoint(pos.x, pos.y);

            drawArea(isHide);
            presenter.areas.push({
                isHide: isHide,
                width: presenter.startSelection.x - presenter.stopSelection.x,
                height: presenter.startSelection.y - presenter.stopSelection.y,
                x: presenter.stopSelection.x,
                y: presenter.stopSelection.y,
                color: presenter.currentLineColor
            });
            presenter.standHideAreaClicked = false;
        };

        presenter.drawAreaLogic_mouseUpCallback = function (event) {
            event.stopPropagation();
            event.preventDefault();

            var pos = getCursorPosition(event);

            presenter.stopSelection = getPoint(pos.x, pos.y);

            drawArea(isHide);
            presenter.areas.push({
                isHide: isHide,
                width: presenter.startSelection.x - presenter.stopSelection.x,
                height: presenter.startSelection.y - presenter.stopSelection.y,
                x: presenter.stopSelection.x,
                y: presenter.stopSelection.y,
                color: presenter.currentLineColor
            });
            presenter.standHideAreaClicked = false;

            presenter.pushStateToStack();
        };

        const $iwbTmpCanvas = presenter.$view.parent().find('.selecting').find('#iwb_tmp_canvas');
        if ( /Android|X11|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) {
            $iwbTmpCanvas.on('touchstart', presenter.drawAreaLogic_touchStartCallback);
            $iwbTmpCanvas.on('touchend', presenter.drawAreaLogic_touchEndCallback);
        } else {
            $iwbTmpCanvas.on(EventsUtils.PointingEvents.TYPES.DOWN, presenter.drawAreaLogic_mouseDownCallback);
            $iwbTmpCanvas.on(EventsUtils.PointingEvents.TYPES.UP, presenter.drawAreaLogic_mouseUpCallback);
        }
    };

    presenter.drawAreaLogic_touchStartCallback = function (event) {
        event.stopPropagation();
        event.preventDefault();
        presenter.standHideAreaClicked = true;
        var pos = getCursorPosition(event.originalEvent);
        presenter.startSelection = getPoint(pos.x, pos.y);
    };

    presenter.drawAreaLogic_mouseDownCallback = function (event) {
        event.stopPropagation();
        event.preventDefault();

        presenter.standHideAreaClicked = true;
        var pos = getCursorPosition(event);
        presenter.startSelection = getPoint(pos.x, pos.y);
    };

    function drawHideArea(context, x, y, width, height, color) {
        context.beginPath();
        context.rect(x, y, width, height);
        context.fillStyle = color;
        context.fill();
        context.lineWidth = 1;
        context.strokeStyle = color;
        context.stroke();
    }

    function drawStandArea(context, x, y, width, height, color) {
        context.clearRect(0, 0, presenter.$selectingMask.width(), presenter.$selectingMask.height());
        context.fillStyle = color;
        context.fillRect(0, 0, presenter.$selectingMask.width(), presenter.$selectingMask.height());
        context.globalCompositeOperation = 'destination-out';
        context.fillStyle = 'white';
        context.beginPath();
        context.rect(x, y, width, height);
        context.fill();
    }

    function drawArea(isHide) {
        var context = presenter.selectingCtx,
            width = presenter.startSelection.x - presenter.stopSelection.x,
            height = presenter.startSelection.y - presenter.stopSelection.y;

        context.globalCompositeOperation = 'source-over';

        if (isHide) {
            drawHideArea(context, presenter.stopSelection.x, presenter.stopSelection.y, width, height, presenter.currentLineColor);
        } else {
            drawStandArea(context, presenter.stopSelection.x, presenter.stopSelection.y, width, height, presenter.currentLineColor);
        }
    }

    function drawSavedAreas() {
        $.each(presenter.areas, function() {
            presenter.selectingCtx.globalCompositeOperation = 'source-over';
            if (this.isHide) {
                drawHideArea(presenter.selectingCtx, this.x, this.y, this.width, this.height, this.color);
            } else {
                drawStandArea(presenter.selectingCtx, this.x, this.y, this.width, this.height, this.color);
            }
        });
    }

    function closeStopwatch() {
        presenter._stopwatch.stopwatch.remove();
        presenter.$panel.find('.stopwatch.clicked').removeClass('clicked');
        presenter.stopwatchAdded = false;
        clearTimeout(presenter._stopwatchTimer);
        presenter._stopwatchTimer = null;
    }

    presenter.createStopwatch = function IWB_Toolbar_createStopwatch(savedStopwatch, hours, minutes, seconds, stopClicked, startClicked) {
        if(!presenter.stopwatchAdded){
            var stopwatch = $('&lt;div class="iwb-toolbar-stopwatch"&gt;&lt;/div&gt;'),
                time = $('&lt;h4 class="stopwatch-time"&gt;&lt;time&gt;00:00:00&lt;/time&gt;&lt;/h4&gt;'),
                header = $('&lt;div class="stopwatch-header"&gt;&lt;/div&gt;'),
                buttons = $('&lt;div class="stopwatch-buttons"&gt;&lt;/div&gt;'),
                startButton = $('&lt;div id="start"&gt;&lt;/div&gt;'),
                stopButton = $('&lt;div id="stop"&gt;&lt;/div&gt;'),
                clearButton = $('&lt;div id="clear"&gt;&lt;/div&gt;'),
                closeButton = $('&lt;div class="stopwatch-close"&gt;&amp;times;&lt;/div&gt;');

            closeButton.on('click', function(e) {
                e.stopPropagation();
                closeStopwatch();
                presenter.pushStateToStack();
            });

            header.append(time);
            header.append(closeButton);
            buttons.append(startButton);
            buttons.append(stopButton);
            buttons.append(clearButton);
            stopwatch.append(header);
            stopwatch.append(buttons);

            var ic_page_height = presenter.$view.parent().height(),
                panel_top = parseInt(presenter.$panel.css('top'), 10),
                window_scroll = $(window).scrollTop(),
                panel_outerHeight = presenter.$panel.outerHeight(true),
                panel_differance = ic_page_height-panel_top-window_scroll,
                top=0;

            var offsetTopelement,
                scrollTop;
            if (presenter.config.panelPosition == 'fixed') {
                offsetTopelement = presenter.$pagePanel.offset().top;
                scrollTop = $(window).scrollTop();
            } else {
                offsetTopelement = '';
                scrollTop = '';
            }

            if (panel_differance &lt; panel_outerHeight) {
                top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - presenter.$pagePanel.offset().top - 120
            } else {
                top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - offsetTopelement
            }

            if (!presenter.stopwatchAdded) {
                stopwatch.draggable({
                    containment: 'parent',
                    opacity: 0.35,
                    create: function(event, _) {
                        $(event.target).css({
                            'top': presenter.getVerticalPosition(savedStopwatch, presenter.POPUP_TYPE.STOPWATCH),
                            ...presenter.getHorizontalPosition(savedStopwatch, presenter.POPUP_TYPE.STOPWATCH),
                            'position' : 'absolute'
                        });
                    },

                    stop: function NoteStopFunction() {
                        $.ui.ddmanager.current = null;
                        presenter.pushStateToStack();
                    }
                });

                presenter.$pagePanel.find('.ic_page').append(stopwatch);
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').click(function(e) {
                    e.stopPropagation();
                });
            }

            var h1 = document.getElementsByClassName('stopwatch-time')[0];
            var start = document.getElementById('start');
            var stop = document.getElementById('stop');
            var clear = document.getElementById('clear');
            presenter.stopButtonClicked = stopClicked;
            presenter.startButtonClicked = startClicked;

            if (seconds) {
                presenter.seconds = seconds; presenter.minutes = minutes; presenter.hours = hours;
                h1.textContent = (presenter.hours ? (presenter.hours &gt; 9 ? presenter.hours : "0" + presenter.hours) : "00") + ":" + (presenter.minutes ? (presenter.minutes &gt; 9 ? presenter.minutes : "0" + presenter.minutes) : "00") + ":" + (presenter.seconds &gt; 9 ? presenter.seconds : "0" + presenter.seconds);

                if (!presenter.stopButtonClicked) {
                    timer();
                    presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#start').addClass('button-clicked');
                }
            } else {
                presenter.seconds = 0; presenter.minutes = 0; presenter.hours = 0;
            }

            function add() {
                presenter.seconds++;
                if (presenter.seconds &gt;= 60) {
                    presenter.seconds = 0;
                    presenter.minutes++;
                    if (presenter.minutes &gt;= 60) {
                        presenter.minutes = 0;
                        presenter.hours++;
                    }
                }

                h1.textContent = (presenter.hours ? addZero(presenter.hours) : "00") + ":" + (presenter.minutes ? addZero(presenter.minutes) : "00") + ":" + addZero(presenter.seconds);

                timer();
            }
            function timer() {
                presenter._stopwatchTimer = setTimeout(add, 1000);
            }

            function clearClickedButtons (){
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#start').removeClass('button-clicked');
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#stop').removeClass('button-clicked');
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#clear').removeClass('button-clicked');
            }

            $(start).on('click', function(){
                if (!presenter.startButtonClicked) {
                    clearClickedButtons();
                    presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#start').addClass('button-clicked');
                    timer();
                    presenter.stopButtonClicked = false;
                    presenter.startButtonClicked = true;
                }
            });

            $(stop).on('click', function() {
                clearClickedButtons();
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#stop').addClass('button-clicked');
                clearTimeout(presenter._stopwatchTimer);
                presenter.stopButtonClicked = true;
                presenter.startButtonClicked = false;
                presenter.pushStateToStack();
            });

            $(clear).on('click', function() {
                clearClickedButtons();
                presenter.$pagePanel.find('.iwb-toolbar-stopwatch').find('#clear').addClass('button-clicked');
                h1.textContent = "00:00:00";
                presenter.seconds = 0; presenter.minutes = 0; presenter.hours = 0;
                presenter.stopButtonClicked = false;
                presenter.pushStateToStack();
            });

            presenter._stopwatch = {
                stopwatch: stopwatch,
                closeButton: closeButton,
                start: start,
                stop: stop,
                clear: clear
            };
        }

        presenter.stopwatchAdded = true;
    };

    presenter.getLeftValueVerticalPopups = function (popupType) {
        const panelLeftValue = parseInt(presenter.$panel.css('left'), 10);
        const toolbarWidth = parseInt(presenter.$panel.outerWidth(), 10);

        return panelLeftValue + toolbarWidth + GAP_VALUE + presenter.getHorizontalOffset(popupType);
    }

    presenter.getLeftValueHorizontalPopups = function (popupType) {
        const panelLeftValue = parseInt(presenter.$panel.css('left'), 10);
        const offset = presenter.getHorizontalPopupOffset(popupType);

        return panelLeftValue + offset;
    }

    presenter.getHorizontalPopupOffset = function (popupType) {
        switch (popupType) {
            case presenter.POPUP_TYPE.STOPWATCH:
                return 0;

            case presenter.POPUP_TYPE.CLOCK:
                return presenter.getStopwatchWidth() + GAP_VALUE;

            case presenter.POPUP_TYPE.NOTE:
                return presenter.getStopwatchWidth() + GAP_VALUE
                    + presenter.getClockWidth() + GAP_VALUE
                    + presenter.notesCounter * GAP_VALUE;
        }
    }

    presenter.getStopwatchWidth = function () {
        const stopwatchWidth = $('.iwb-toolbar-stopwatch').outerWidth();
        return stopwatchWidth ? stopwatchWidth : 180;
    }

    presenter.getClockWidth = function () {
        const clockWidth = $('.iwb-toolbar-clock').outerWidth();
        return clockWidth ? clockWidth : 100;
    }

    presenter.getRightValueVerticalPopups = function (popupType) {
        const panelRightValue = parseInt(presenter.$panel.css('right'), 10);
        const toolbarWidth = parseInt(presenter.$panel.outerWidth(), 10);

        return panelRightValue + toolbarWidth + GAP_VALUE + presenter.getHorizontalOffset(popupType);
    }

    presenter.getTopValue = function (popupType) {
        const panelTopValue = parseInt(presenter.$panel.css('top'), 10);
        let offset;
        if (presenter.config.popupsDirection === 'left' || presenter.config.popupsDirection === 'right') {
            offset = presenter.getVerticalOffset(popupType);
        } else if (presenter.config.popupsDirection === 'down') {
            const toolbarHeight = parseInt(presenter.$panel.outerHeight(), 10);
            const noteOffset = popupType === presenter.POPUP_TYPE.NOTE ? presenter.notesCounter * GAP_VALUE : 0;
            offset = toolbarHeight + GAP_VALUE + noteOffset;
        } else if (presenter.config.popupsDirection === 'up') {
            const noteOffset = popupType === presenter.POPUP_TYPE.NOTE ? presenter.notesCounter * GAP_VALUE : 0;
            offset = -(presenter.getPopupHeight(popupType) + GAP_VALUE + noteOffset);
        }

        return panelTopValue + offset;
    }

    presenter.getPopupHeight = function (popupType) {
        switch (popupType) {
            case presenter.POPUP_TYPE.STOPWATCH:
                return STOPWATCH_OFFSET;

            case presenter.POPUP_TYPE.CLOCK:
                return CLOCK_OFFSET - 8;

            case presenter.POPUP_TYPE.NOTE:
                return NOTE_OFFSET;
        }
    }

    presenter.getVerticalOffset = function (popupType) {
        switch (popupType) {
            case presenter.POPUP_TYPE.STOPWATCH:
                return 0;

            case presenter.POPUP_TYPE.CLOCK:
                return CLOCK_OFFSET + GAP_VALUE;

            case presenter.POPUP_TYPE.NOTE:
                return NOTE_OFFSET + GAP_VALUE + presenter.notesCounter * GAP_VALUE;
        }
    }

    presenter.getHorizontalOffset = function (popupType) {
        switch (popupType) {
            case presenter.POPUP_TYPE.CLOCK:
            case presenter.POPUP_TYPE.STOPWATCH:
                return 0;

            case presenter.POPUP_TYPE.NOTE:
                return presenter.notesCounter * GAP_VALUE;
        }
    }

    presenter.getVerticalPosition = function (savedElement, popupType) {
        if (savedElement) {
            return savedElement.top
        }

        return presenter.getTopValue(popupType)
    }

    presenter.getHorizontalPosition = function (savedElement, popupType) {
        if (savedElement) {
            return {'left' : savedElement.left}
        }
        switch (presenter.config.popupsDirection) {
            case 'down':
            case 'up':
                return {'left': presenter.getLeftValueHorizontalPopups(popupType)};

            case 'right':
                return {'left': presenter.getLeftValueVerticalPopups(popupType)};

            case 'left':
               return {'right': presenter.getRightValueVerticalPopups(popupType)};
        }
    }

    function closeClock() {
        presenter._clockwatch.clock.remove();
        presenter.$panel.find('.clock.clicked').removeClass('clicked');
        presenter.clockAdded = false;
    }

    presenter.createClock = function IWB_Toolbar_createClock(savedClock) {
        if (!presenter.clockAdded) {
            var clock = $('&lt;div class="iwb-toolbar-clock"&gt;&lt;/div&gt;'),
                header = $('&lt;div class="clock-header"&gt;&lt;/div&gt;'),
                closeButton = $('&lt;div class="clock-close"&gt;&amp;times;&lt;/div&gt;'),
                clockBody = $('&lt;div class="clock-body"&gt;&lt;/div&gt;');

            closeButton.on('click', function(e) {
                e.stopPropagation();
                closeClock();
                presenter.pushStateToStack();
            });

            header.append(closeButton);
            clock.append(header);
            clock.append(clockBody);

            var ic_page_height = presenter.$view.parent().height(),
                panel_top = parseInt(presenter.$panel.css('top'), 10),
                window_scroll = $(window).scrollTop(),
                panel_outerHeight = presenter.$panel.outerHeight(true),
                panel_differance = ic_page_height-panel_top-window_scroll,
                top=0;

            var offsetTopelement,
                scrollTop;
            if (presenter.config.panelPosition == 'fixed') {
                offsetTopelement = presenter.$pagePanel.offset().top;
                scrollTop = $(window).scrollTop();
            } else {
                offsetTopelement = '';
                scrollTop = '';
            }

            if (panel_differance &lt; panel_outerHeight) {
                top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - presenter.$pagePanel.offset().top - 120
            } else {
                top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - offsetTopelement
            }

            clock.draggable({
                containment: 'parent',
                opacity: 0.35,
                create: function(event, _) {
                    $(event.target).css({
                        'top': presenter.getVerticalPosition(savedClock, presenter.POPUP_TYPE.CLOCK),
                        ...presenter.getHorizontalPosition(savedClock, presenter.POPUP_TYPE.CLOCK),
                        'position' : 'absolute'
                    });
                },
                stop: function NoteStopFunction() {
                    $.ui.ddmanager.current = null;
                    presenter.pushStateToStack();
                }
            });

            presenter.$pagePanel.find('.ic_page').append(clock);
            presenter.$pagePanel.find('.iwb-toolbar-clock').click(function(e) {
                e.stopPropagation();
            });

            function getTime() {
                var date = new Date();
                return addZero(date.getHours()) + ':' + addZero(date.getMinutes()) + ':' + addZero(date.getSeconds());
            }

            presenter.$pagePanel.find('.iwb-toolbar-clock').find('.clock-body').html(getTime());

            presenter._clockwatchTimer = setInterval(function() {
                presenter.$pagePanel.find('.iwb-toolbar-clock').find('.clock-body').html(getTime());
            }, 1000);

            presenter._clockwatch = {
                clock: clock,
                closeButton: closeButton
            };
        }
        presenter.clockAdded = true;
    };

    presenter.Note = function () {
        this.$note = null;
        this.$header = null;
        this.$date = null;
        this.$closeButton = null;
        this.$noteBody = null;
        this.$textarea = null;
        this.$buttonSave = null;
        this.currentValue = '';
    };


    presenter.Note._internals = {};

    presenter.Note._internals.getCurrentDate = function () {
        var day = new Date().getDate(),
            month = new Date().getMonth() + 1,
            year = new Date().getFullYear(),
            hours = new Date().getHours(),
            minutes = new Date().getMinutes();

        var time = addZero(hours) + ':' + addZero(minutes);
        return day + '/' + month + '/' + year + ', ' + time;
    };

    presenter.Note._internals.createView = function () {
        this.$note = $('&lt;div class="iwb-toolbar-note"&gt;&lt;/div&gt;');
        this.$header = $('&lt;div class="note-header"&gt;&lt;/div&gt;');
        this.$date = $('&lt;div class="note-date"&gt;&lt;/div&gt;');
        this.$closeButton = $('&lt;div class="note-close"&gt;&amp;times;&lt;/div&gt;');
        this.$noteBody = $('&lt;div class="note-body"&gt;&lt;/div&gt;');

        this.$header.append(this.$date);
        this.$header.append(this.$closeButton);
        this.$note.append(this.$header);
        this.$note.append(this.$noteBody);
    };

    presenter.Note.prototype.init = function (savedNote) {
        presenter.Note._internals.createView.call(this);

        this.connectHandlers();
        this.connectDraggable(savedNote);
    };

    presenter.Note.prototype.setDate = function (date) {
        this.$date.html(date);
    };

    presenter.Note.prototype.setBody = function (body) {
        this.$noteBody.html(body);
        this.$textarea = this.$noteBody.find('textarea');
        this.$buttonSave = this.$noteBody.find('.save');
    };

    presenter.Note.prototype.getView = function () {
        return this.$note;
    };

    presenter.Note.prototype.connectNoteEditHandler = function () {
        this.$note.on('dblclick', function () {
            this.noteEditHandler();
            this.$note.off('dblclick');
        }.bind(this));

        const foundTouchPoints = (navigator.maxTouchPoints &gt; 0 || navigator.msMaxTouchPoints &gt; 0);
        if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || foundTouchPoints) {
            window.EventsUtils.DoubleTap.on(this.$note, function () {
                this.noteEditHandler();
                window.EventsUtils.DoubleTap.off(this.$note);
            }.bind(this));
        }
    };

    presenter.Note.prototype.saveButtonHandler = function() {
        var value = this.$textarea.val();
        this.$noteBody.html(value);
        this.$textarea.remove();
        this.connectNoteEditHandler();
        presenter.pushStateToStack();
    };

    presenter.Note.prototype.noteEditHandler = function () {
        this.$textarea = $('&lt;textarea&gt;&lt;/textarea&gt;');
        this.$buttonSave = $('&lt;button class="save"&gt;Save&lt;/button&gt;');

        this.currentValue = this.$noteBody.html();

        if(MobileUtils.isEventSupported('touchstart')) {
            this.$buttonSave.on('touchstart', function() {
                this.saveButtonHandler();
            }.bind(this));
        } else {
            this.$buttonSave.on('click', function() {
                this.saveButtonHandler();
            }.bind(this));
        }

        this.$textarea.on('click', function (){
            var val = this.$textarea.val();
            this.$textarea.focus().val("").val(val);
        }.bind(this));

        this.$textarea.val(this.currentValue);

        this.$noteBody.html(this.$textarea);
        this.$noteBody.append(this.$buttonSave);
        this.$textarea.focus();
    };

    presenter.Note.prototype.closeButtonHandler = function(event) {
        var confirmation = presenter.$removeConfirmationBox;
        var window_scroll = presenter.playerController.iframeScroll() &gt; 0 ? presenter.playerController.iframeScroll() : $(window).scrollTop();

        confirmation.css('top', window_scroll + 10 + 'px');
        confirmation.show();
        confirmation.find('.no-button').on(getTouchStartOrMouseDownEventName(),function(e) {
            e.stopPropagation();
            confirmation.hide();
        });
        confirmation.find('.yes-button').on(getTouchStartOrMouseDownEventName(), {"note": event.data.note}, function(e) {
            e.stopPropagation();
            var note = e.data.note;

            presenter.noteObjects = presenter.noteObjects.filter(function (note) {
                return note != this;
            }, note);
            note.destroy();
            confirmation.hide();
            presenter.pushStateToStack();
        });
    };

    presenter.Note.prototype.connectHandlers = function () {
        if(MobileUtils.isEventSupported('touchstart')) {
            this.$closeButton.on('touchstart', {"note": this}, function(event) {
                event.stopPropagation();
                this.closeButtonHandler(event);
            }.bind(this));
        } else {
            this.$closeButton.on('click', {"note": this}, function(event) {
                event.stopPropagation();
                this.closeButtonHandler(event);
            }.bind(this));
        }

        this.connectNoteEditHandler();
    };

    presenter.Note.prototype.connectDraggable = function (savedNote) {
        var ic_page_height = presenter.$view.parent().height(),
            panel_top = parseInt(presenter.$panel.css('top'), 10),
            window_scroll = presenter.playerController.iframeScroll() &gt; 0 ? presenter.playerController.iframeScroll() : $(window).scrollTop(),
            panel_outerHeight = presenter.$panel.outerHeight(true),
            panel_differance = ic_page_height-panel_top-window_scroll,
            top=0;

        var offsetTopelement,
            scrollTop;
        if (presenter.config.panelPosition == 'fixed') {
            offsetTopelement = presenter.$pagePanel.offset().top;
            scrollTop = window_scroll;
        } else {
            offsetTopelement = '';
            scrollTop = '';
        }

        if (panel_differance &lt; panel_outerHeight) {
            top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - presenter.$pagePanel.offset().top - 120;
        } else {
            top = parseInt(presenter.$panel.css('top'), 10) + presenter.$panel.outerHeight(true) + scrollTop - offsetTopelement;
        }

        this.$note.draggable({
            containment: 'parent',
            opacity: 0.35,
            create: function(event, _) {
                $(event.target).css({
                    'top': presenter.getVerticalPosition(savedNote, presenter.POPUP_TYPE.NOTE),
                    ...presenter.getHorizontalPosition(savedNote, presenter.POPUP_TYPE.NOTE),
                    'position' : 'absolute'
                });
            },

            stop: function NoteStopFunction() {
                $.ui.ddmanager.current = null;
                presenter.pushStateToStack();
            }
        });
        presenter.notesCounter += 1;
    };

    presenter.Note.prototype.destroy = function () {
        if(this.$note) {
            var $note = this.$note.draggable("destroy");
            $note.off();
            this.$note.off();
            this.$header.off();
            this.$date.off();
            this.$closeButton.off();
            this.$noteBody.off();
            if (this.$textarea !== null) {
                this.$textarea.off();
            }

            if (this.$buttonSave !== null) {
                this.$buttonSave.off();
            }

            window.EventsUtils.DoubleTap.off(this.$note);

            this.$note.remove();
            this.$note = null;
            this.$header = null;
            this.$date = null;
            this.$closeButton = null;
            this.$noteBody = null;
            this.$textarea = null;
            this.$buttonSave = null;

            presenter.notesCounter -= 1;
        }
    };

    presenter.Note.prototype.getState = function () {
        return {
            'top': this.$note.css('top'),
            'left': this.$note.css('left'),
            'date': this.$date.html(),
            'body': this.$noteBody.html()
        };
    };

    presenter.Note.createNote = function (savedNote) {
        var note = new presenter.Note();
        note.init(savedNote);

        if (savedNote) {
            note.setDate(savedNote.date);
            note.setBody(savedNote.body);
        }

        return note;
    };

    presenter.createNote = function IWB_Toolbar_createNote(savedNote) {
        return presenter.Note.createNote(savedNote);
    };


    presenter.executeUserEventCode = function (eventCode) {
        presenter.playerController.getCommands().executeEventCode(eventCode);
    };

    presenter.runCustomScript = function IWB_Toolbar_runCustomScript() {
        var eventCode = presenter.isCustomButtonActive() ? presenter.config.onCustomButtonSelected : presenter.config.onCustomButtonDeselected;
        presenter.executeUserEventCode(eventCode);
    };


    presenter.zoomSelectedModule = function IWB_Toolbar_zoomSelectedModule(event) {
        var selectedModule = event.currentTarget;
        if (presenter.$pagePanel.find('.zoomed').length &gt; 0) {
            presenter.$panel.show();
            zoom.out();
            $(selectedModule).parent().find('.zoomed').removeClass('zoomed');
            presenter.changeCursor('zoom-in');
        } else {
            presenter.$panel.hide();
            var topWindowHeight = 0;
            var iframeTopOffset = 0;
            if (window.iframeSize) {
                topWindowHeight = window.iframeSize.windowInnerHeight;
                iframeTopOffset = window.iframeSize.offsetTop - window.iframeSize.frameOffset;
            }
            if ($(selectedModule).hasClass('iwb-default-zoom-cover')) {
                var y = event.clientY;
                var minY = (presenter.zoomConfiguration.initialWindowHeight/2 - window.iframeSize.frameOffset) / presenter.config.defaultZoom;
                var pageHeight = $('.ic_page').height();
                if ($('.ic_header').length &gt; 0) pageHeight += $('.ic_header').height();
                if ($('.ic_footer').length &gt; 0) pageHeight += $('.ic_footer').height();
                var maxY = pageHeight - (presenter.zoomConfiguration.initialWindowHeight/2 / presenter.config.defaultZoom);
                if (y &lt; minY) y = minY;
                if (y &gt; maxY) y = maxY;

                zoom.to({
                    x: event.clientX,
                    y: y,
                    scale: presenter.config.defaultZoom,
                    topWindowHeight: topWindowHeight,
                    iframeTopOffset: iframeTopOffset
                });
            } else {
                zoom.to({
                    element: selectedModule,
                    topWindowHeight: topWindowHeight,
                    iframeTopOffset: iframeTopOffset
                });
            }
            $(selectedModule).addClass('zoomed');
            presenter.changeCursor('zoom-out');
        }
    };

    presenter.changeCursor = function(type) {
        presenter.$pagePanel.removeClass('iwb-zoom-in iwb-zoom-out');

        switch (type) {
            case 'zoom-in':
                presenter.$pagePanel.addClass('iwb-zoom-in');
                break;
            case 'zoom-out':
                presenter.$pagePanel.addClass('iwb-zoom-out');
                break;
        }
    };

    function isDependingOnDrawing(button) {
        return $(button).hasClass('color') || $(button).hasClass('thickness');
    }

    presenter.isDrawingActive = function IWB_Toolbar_isDrawingActive() {
        return presenter.$pagePanel.find('.button.pen.clicked, .button.marker.clicked').length &gt; 0;
    };

    presenter.isAreaDrawingActive = function IWB_Toolbar_isAreaDrawingActive() {
        return presenter.$pagePanel.find('.button.hide-area.clicked, .button.stand-area.clicked').length &gt; 0
            || presenter.drawMode == presenter.DRAW_MODE.HIDE_AREA;
    };

    presenter.areDrawingButtonsActive = function () {
        return presenter.isDrawingActive() || presenter.isAreaDrawingActive();
    };

    function isFloatingImageButton(button) {
        return $(button).hasClass('floating-image');
    }

    function shouldHideDrawingMasks(button) {
        return !$(button).hasClass('pen') &amp;&amp; !$(button).hasClass('marker') &amp;&amp; !$(button).hasClass('eraser');
    }

    function shouldHideSelectingMasks(button) {
        return !$(button).hasClass('stand-area') &amp;&amp; !$(button).hasClass('hide-area');
    }

    function shouldHideFloatingImage(button) {
        return !$(button).hasClass('reset') &amp;&amp; !$(button).hasClass('floating-image');
    }

    function shouldClosePanelsOnReset(button) {
        return !$(button).hasClass('pen') &amp;&amp; !$(button).hasClass('marker');
    }

    function changeBottomButtonState(button) {
        presenter.$panel.find('.container .clicked-lighter').removeClass('clicked-lighter');
        $(button).toggleClass('clicked-lighter');
    }

    presenter.isSupportCSSPointerEvents = function IWB_Toolbar_isSupportCSSPointerEvents() {
        var myNav = navigator.userAgent.toLowerCase();
        var version = (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
        return !(version == 9 || version == 10);
    };

    presenter.toogleMasks = function IWB_Toolbar_toggleMasks() {
        if (!presenter.isSupportCSSPointerEvents()) {
            presenter.$penMask.hide();
            presenter.$markerMask.hide();
        }

        presenter.$selectingMask.hide();
        if (presenter.isDrawingActive() || presenter.$pagePanel.find('.eraser').hasClass('clicked')) {
            presenter.$penMask.show();
            presenter.$markerMask.show();
        }

        if (presenter.isAreaDrawingActive()) {
            presenter.$selectingMask.show();
        }
    };

    presenter.toggleBottomPanels = function IWB_Toolbar_toggleBottomPanels() {
        var $thicknessPanel = presenter.$pagePanel.find('.bottom-panel-thickness');
        var $colorPanel = presenter.$pagePanel.find('.bottom-panel-color');

        $thicknessPanel.hide();
        $colorPanel.hide();

        function showPanel($panel) {
            if ($panel === 'COLOR') {
                $colorPanel.show();
            } else if ($panel === 'THICKNESS') {
                $thicknessPanel.show();
            } else {
                // NONE
            }
        }

        if (presenter.$pagePanel.find('.pen').hasClass('clicked')) {
            showPanel(presenter.config.showForPen);
        }

        if (presenter.$pagePanel.find('.marker').hasClass('clicked')) {
            showPanel(presenter.config.showForMarker);
        }
    };

    presenter.createCanvases = function () {
        presenter.createCanvas(
            function(mask) {
                presenter.$markerMask = mask;
                presenter.$markerMask.addClass('marker-mask');
                return presenter.$markerMask;
            },
            function(ctx) {
                presenter.markerCtx = ctx;
            },
            function(canvas) {
                presenter.$markerCanvas = canvas;
            }
        );

        presenter.createCanvas(
            function(mask) {
                presenter.$penMask = mask;
                presenter.$penMask.addClass('pen-mask');
                return presenter.$penMask;
            },
            function(ctx) {
                presenter.ctx = ctx;
            },
            function(canvas) {
                presenter.$canvas = canvas;
            }
        );

        presenter.createCanvas(
            function(mask) {
                presenter.$selectingMask = mask;
                presenter.$selectingMask.addClass('selecting');
                return presenter.$selectingMask;
            },
            function(ctx) {
                presenter.selectingCtx = ctx
            },
            function(canvas) {
                presenter.selectingCanvas = canvas;
            }
        );
    };

    presenter.createCanvas = function (setMask, setContext, setCanvas) {
        var $mask = $('&lt;div class="iwb-toolbar-mask"&gt;&lt;/div&gt;');
        $mask = setMask($mask);
        $mask.hide();

        var icPage = presenter.$pagePanel.find('.ic_page');
        icPage.css('position', 'relative');
        icPage.append($mask);

        var canvas = $('&lt;canvas&gt;&lt;/canvas&gt;');
        setCanvas(canvas);
        setContext(canvas[0].getContext("2d"));

        $mask.append(canvas);
        canvas[0].width = $mask.width();
        canvas[0].height = $mask.height();
    };

    presenter.applyHovered = function (elements) {
        $.each(elements, function(_, btn) {
            presenter._hoveredButtons.push($(btn));
            $(btn).hover(function() {
                $(this).addClass('hovered');
                $(this).find('.tooltip').show();
            }, function() {
                $(this).removeClass('hovered');
                $(this).find('.tooltip').hide();
            });
        });
    };

    presenter.createPreview = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.model = model;
        presenter.isKeepStateAndPosition = ModelValidationUtils.validateBoolean(model['keepStateAndPosition']);

        $(view).find('.iwb-toolbar-panel').width(model['Width'] - 50 + 'px');
        $(view).find('.iwb-toolbar-panel').addClass(presenter.model['expansionDirection']);

        var moduleClasses = $(view).attr('class');

        if (moduleClasses.indexOf('addon_IWB_Toolbar') &lt; 0){
            var moduleCustomClass =  moduleClasses.replace('ice_module', '');
            $(view).find('.iwb-toolbar-panel').addClass(moduleCustomClass);
            $(view).removeClass(moduleCustomClass);
        }else{
            $(view).find('.iwb-toolbar-panel').addClass('addon_IWB_Toolbar');
            $(view).removeClass('addon_IWB_Toolbar');
        }
    };

    presenter.run = function(view, model) {
        Kinetic.pixelRatio = 1;
        model = presenter.upgradeModel(model);
        presenter.model = model;
        presenter.isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.isKeepStateAndPosition = ModelValidationUtils.validateBoolean(model['keepStateAndPosition']);

        presenter.headerLoadedDeferred = new $.Deferred();
        presenter.headerLoaded = presenter.headerLoadedDeferred.promise();


        presenter.setBasicConfiguration(view, model);

        if (!presenter.config.isValid) {
            DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, presenter.config.errorCode);
            return;
        }

        if (!presenter.config.enableUndoRedo) {
            presenter.$pagePanel.find('.reset-one').css('display', 'none');
            presenter.$pagePanel.find('.redo-one').css('display', 'none');
        }

        presenter.addFloatingImages(model);

        presenter.createCanvases();

        presenter.markerTmpCanvas = document.createElement('canvas');
        presenter.markerTmpCtx = presenter.markerTmpCanvas.getContext('2d');
        presenter.markerTmpCanvas.classList.add('iwb_tmp_canvas');

        presenter.$panel.draggable({
            containment: 'parent',
            opacity: 0.35,
            create: function (event, _) {
                $(event.target).addClass('running');
                $(event.target).css('position', presenter.config.panelPosition);
                if (window.savedPanel &amp;&amp; window.savedPanel.position) {
                    if (presenter.isKeepStateAndPosition) {
                        if (presenter.config.panelPosition == 'fixed') {
                            $(event.target).css('top', window.savedPanel.position.top + presenter.$pagePanel.offset().top + 'px');
                            $(event.target).css('left', window.savedPanel.position.left + presenter.$pagePanel.offset().left + 'px');
                        } else {
                            $(event.target).css('top', window.savedPanel.position.top + 'px');
                            $(event.target).css('left', window.savedPanel.position.left + 'px');
                        }
                    } else {
                        if (presenter.config.panelPosition == 'fixed') {
                            $(event.target).css('top', (parseInt(model['Top'], 10)) + presenter.$pagePanel.offset().top + 'px');
                            $(event.target).css('left', (parseInt(model['Left'], 10)) + presenter.$pagePanel.offset().left + 'px');

                        } else {
                            $(event.target).css('top', (parseInt(model['Top'], 10)) + 'px');
                            $(event.target).css('left', (parseInt(model['Left'], 10)) + 'px');
                        }
                    }
                } else {
                    var offsetTopPrev,
                        offsetLeftPrev;
                    if (presenter.config.panelPosition == 'fixed') {
                        offsetTopPrev = presenter.$pagePanel.offset().top;
                        offsetLeftPrev = presenter.$pagePanel.offset().left;
                    } else {
                        offsetTopPrev = $(presenter.$panel).position().top;
                        offsetLeftPrev = $(presenter.$panel).position().left;
                    }
                    $(event.target).css('top', (offsetTopPrev + parseInt(model['Top'], 10)) + 'px');
                    $(event.target).css('left', (offsetLeftPrev + parseInt(model['Left'], 10)) + 'px');
                    presenter.headerLoaded.then(function () {
                        var offsetTop,
                            offsetLeft;
                        if (presenter.config.panelPosition == 'fixed') {
                            offsetTop = presenter.$pagePanel.offset().top;
                            offsetLeft = presenter.$pagePanel.offset().left;
                        } else {
                            offsetTop = '';
                            offsetLeft = '';
                        }
                        $(event.target).css('top', (offsetTop + parseInt(model['Top'], 10)) + 'px');
                        $(event.target).css('left', (offsetLeft + parseInt(model['Left'], 10)) + 'px');
                    });
                }
            },
            stop: function (event, ui) {
                var top = ui.position.top;
                var left = ui.position.left;

                if (presenter.config.panelPosition == 'fixed') {
                    window.savedPanel.position = { top: top - presenter.$pagePanel.offset().top, left: left - presenter.$pagePanel.offset().left};
                } else {
                    window.savedPanel.position = { top: top, left: left};
                }

                $.ui.ddmanager.current = null;

                presenter.zoomConfiguration.viewLeftOffset = presenter.$panel.offset().left;
            }
        });


        presenter.applyHovered([presenter.$panel.find('.button')]);

        var newWidthWhenClosed = presenter.config.widthWhenClosed - 50;
        presenter.trueClosedPanelSize = {
            width: newWidthWhenClosed &lt; 0 ? presenter.$panel.width() : newWidthWhenClosed,
            height: presenter.$panel.height()
        }
        if (presenter.config.expansionDirection == "left" || presenter.config.expansionDirection == "right") {
            presenter.$panel.width(presenter.trueClosedPanelSize.width + 'px');
        } else {
            presenter.$panel.css("min-width", presenter.trueClosedPanelSize.width + 'px');
        }

        window.savedPanel = window.savedPanel || {};

        if (window.savedPanel &amp;&amp; window.savedPanel.isOpen &amp;&amp; presenter.isKeepStateAndPosition) {
            presenter.openPanel(false);
        } else {
            window.savedPanel.widthWhenOpened = presenter.config.widthWhenOpened;
        }

        presenter.addEventHandlers();


        if ((presenter.isInFrame || MobileUtils.isMobileUserAgent(navigator.userAgent)) &amp;&amp; presenter.config.panelPosition == 'fixed') {
            addScrollHandler();
        }

        $(view).hide();
        presenter.setVisibility(presenter.isVisible, false, view);

        var width = presenter.$pagePanel.find('.marker-mask').find('canvas')[0].width;
        var height = presenter.$pagePanel.find('.marker-mask').find('canvas')[0].height;

        presenter.markerTmpCanvas.width = width;
        presenter.markerTmpCanvas.height = height;
        presenter.$pagePanel.find('.marker-mask').append(presenter.markerTmpCanvas);

        /**
         * We're adding addon class to its panel as a way of ensuring custom class styling applies.
         * Normally addon has custom class set by Player, but in our case the view is hidden and visible part
         * (panel) is not a child of it.
         */
        presenter.$panel.addClass(document.getElementById(model["ID"]).className);

        zoom.init();
        if(presenter.isKeepStateAndPosition){
           if(window.savedPanel.tools){
               presenter.activeFunction = window.savedPanel.tools.activeFunction;
               presenter.customButtonActive = window.savedPanel.tools.customButtonActive;
               presenter.closePenColor = window.savedPanel.tools.stateColor;
               presenter.closePenThickness = window.savedPanel.tools.stateThickness;
               presenter.buttonColor = presenter.$bottomPanels.find('[color*='+window.savedPanel.tools.buttonColor+']')[0];
               presenter.buttonThickness = presenter.$bottomPanels.find('[thickness*='+window.savedPanel.tools.buttonThickness+']')[0];
               presenter.isCloseColor = window.savedPanel.tools.isCloseColor;
               presenter.shouldSaveColor = window.savedPanel.tools.shouldSaveColor;
                   if(presenter.activeFunction){
                       if(presenter.activeFunction != 'clock'
                       &amp;&amp; presenter.activeFunction != 'stopwatch'
                       &amp;&amp; presenter.activeFunction != 'note'
                       &amp;&amp; presenter.activeFunction != 'reset'
                       &amp;&amp; presenter.activeFunction != 'open'
                       &amp;&amp; presenter.activeFunction != 'reset-one'
                       &amp;&amp; presenter.activeFunction != 'redo-one'){
                           if(!presenter.recklick){
                               presenter.functionButton = presenter.$pagePanel.find('.'+ presenter.activeFunction);
                               if(window.savedPanel.isOpen){
                                   presenter.buttonsLogic[presenter.activeFunction].onOpen(presenter.functionButton);
                                   presenter.functionButton.addClass('clicked');
                               }
                           }
                           presenter.isRecklicked = false;
                       }
                   }
                   if (presenter.customButtonActive) {
                        var button = presenter.$pagePanel.find('.' + CUSTOM_BUTTON_NAME);
                        if(window.savedPanel.isOpen) {
                            button.addClass('clicked');
                        }
                   }
               if(presenter.isCloseColor){
                   presenter.changeColor(presenter.closePenColor, presenter.buttonColor);
                   presenter.changeThickness(presenter.closePenThickness, presenter.buttonThickness);
               }
           }
        }

        presenter._view = view;

        presenter.updateZoomConfiguration();
    };

    presenter.onDestroy = function () {
        var isOpenPanel = window.savedPanel.isOpen;
        if (isOpenPanel) {
            presenter.closePanel(false);
            window.savedPanel.isOpen = true;
        }
        clearCanvases();

        presenter.points = [];
        presenter.points = null;
        presenter.mouse = null;

        /***
         * **********************************************
         * addEventHandlers
         * **********************************************
         */
        presenter.$pagePanel.find('.iwb-toolbar-mask').off();
        presenter.$pagePanel.find('.note').off();

        //noteObjects
        if(presenter.noteObjects) {
            presenter.noteObjects.forEach(function (note) {
                note.destroy();
                note = null;
            });
        }
        presenter.noteObjects = [];
        presenter.noteObjects = null;

        presenter.Note.prototype.getView = null;
        presenter.Note.createNote = null;
        presenter.Note = null;
        presenter.createNote = null;

        //stopwatch
        if (presenter._stopwatch !== null) {
            presenter._stopwatch.stopwatch.off();
            presenter._stopwatch.stopwatch.draggable('destroy');
            presenter._stopwatch.closeButton.off();
            $(presenter._stopwatch.start).off();
            $(presenter._stopwatch.stop).off();
            $(presenter._stopwatch.clear).off();

            if (presenter._stopwatchTimer !== null) {
                clearTimeout(presenter._stopwatchTimer);
            }

            presenter._stopwatch.stopwatch = null;
            presenter._stopwatch.closeButton = null;
            presenter._stopwatch.start = null;
            presenter._stopwatch.stop = null;
            presenter._stopwatch.clear = null;
            presenter._stopwatchTimer = null;
            presenter._stopwatch = null;
        }

        //clock
        if (presenter._clockwatch !== null) {

            if (presenter._clockwatchTimer !== null) {
                clearInterval(presenter._clockwatchTimer);
            }

            presenter._clockwatch.clock.off();
            presenter._clockwatch.clock.draggable('destroy');
            presenter._clockwatch.closeButton.off();

            presenter._clockwatchTimer = null;
            presenter._clockwatch.clock = null;
            presenter._clockwatch.closeButton = null;
            presenter._clockwatch = null;
        }

        presenter.$pagePanel.find('.button').off();
        presenter.$defaultColorButton.off();
        presenter._iwb_buttons.forEach(function ($btn) {
           $btn.off();
        });

        presenter.$pagePanel.off();
        presenter.$panel.off();


        presenter._iwb_buttons.length = 0;
        presenter._iwb_buttons = null;

        presenter.buttonClicked = null;
        presenter.isSavedState = null;
        presenter.buttonsLogic = null;
        presenter.recklick = null;

        $(presenter.markerTmpCanvas).off();
        presenter.$canvas.off(`${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.UP} touchstart touchmove touchend`);
        presenter.$markerCanvas.off(`${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.UP} touchstart touchmove touchend`);

        presenter.isZoomActive = null;
        presenter.$defaultColorButton = null;
        presenter.currentLineColor = null;
        presenter.drawMode = null;

        //changeThickness
        presenter.currentLineWidth = null;
        presenter.currentMarkerThickness = null;
        presenter.currentEraserThickness = null;


        //penCloseHandler
        presenter.penLineColor = null;
        presenter.penLineWidth = null;
        presenter.penColorBackground = null;
        presenter.penThicknessBackground = null;

        //penUnclickHandler
        presenter.colorBackground = null;
        presenter.penClicked = null;

        presenter.markerColorBackground = null;
        presenter.markerThicknessBackground = null;
        presenter.markerCtx = null;
        presenter.markerClicked = null;
        presenter.markerTmpCanvas = null;

        //markerCloseHandler
        presenter.markerLineColor = null;
        presenter.markerLineWidth = null;

        //markerUnclickHandler
        //pass

        //defaultClickHandler
        //pass

        //thicknessClickHandler
        //pass

        //zoomClickHandler
        $(".iwb-zoom-cover").off();
        if (presenter.modules !== undefined) {
            presenter.modules.find('a').unbind('click', presenter.preventClickAction_zoomClickHandler);
            presenter.modules.length = 0;
            presenter.modules = null;
        }

        //textAudioEvents
        presenter.textAudioEvents.forEach(function (element) {
            $(element).unbind('click', preventClickActionTextAudio);
        });
        presenter.areZoomEventHandlersAttached = null;
        presenter.isPointerDown = null;

        //eraserClickHandler
        presenter.ctx = null;
        presenter.drawMode = null;

        //eraserUnClickHandler
        presenter.$penMask = null;

        //hideAreaClickHandler
        var $hideAreaCanvas = presenter.$view.parent().find('.selecting').find('#iwb_tmp_canvas');
        $hideAreaCanvas.off();

        //standAreaClickHandler
        //pass

        //resetClickHandler
        presenter.selectingCtx = null;
        presenter.areas = [];
        presenter.areas = null;

        //noteClickHandler
        //pass

        //customScriptClickHandler
        //pass

        //floatingImageClickHandler
        //pass

        //clockClickHandler
        //pass

        //stopwatchClickHandler
        //pass

        //closeClickHandler
        presenter.closePenColor = null;
        presenter.currentLineColor = null;
        presenter.closePenThickness = null;
        presenter.currentLineWidth = null;
        presenter.isCloseColor = null;
        presenter.shouldSaveColor = null;
        presenter.closePenColor = null;
        presenter.closePenThickness = null;
        presenter.currentMarkerThickness = null;
        presenter.isCloseColor = null;
        presenter.isCloseColor = null;
        presenter.isPanelOpened = null;
        presenter.activeFunction = null;
        presenter.activeButton = null;
        presenter.customButtonActive = null;

        //openClickHandler
        presenter.isRecklicked = null;

        /***
         * *******************************************************************
         * setBasicConfiguration
         * *******************************************************************
         */
        presenter.$removeConfirmationBox.off();
        presenter.$removeConfirmationBoxClock.off();
        presenter.$removeConfirmationBoxStopwatch.off();
        presenter.$panel.draggable("destroy");
        presenter.$view.off();
        presenter.$defaultThicknessButton.off();
        presenter.$buttonsExceptOpen.off();
        presenter.$removeConfirmationBox.off();
        presenter.$removeConfirmationBoxClock.off();
        presenter.$removeConfirmationBoxStopwatch.off();
        presenter.$bottomPanels.off();

        presenter.$removeConfirmationBox.remove();
        presenter.$removeConfirmationBoxClock.remove();
        presenter.$removeConfirmationBoxStopwatch.remove();
        presenter.$panel.remove();

        presenter.headerLoadedDeferred = null;
        presenter.headerLoaded = null;

        presenter.$view = null;
        presenter.$panel = null;
        presenter.$defaultThicknessButton = null;
        presenter.isInFrame = null;
        presenter.$buttonsExceptOpen = null;
        presenter.buttonWidth = null;

        presenter.$removeConfirmationBox = null;

        presenter._view = null;
        presenter.model = null;
        presenter.isVisible = null;
        presenter.isKeepStateAndPosition = null;

        presenter.$pagePanel = null;
        presenter.$removeConfirmationBoxClock = null;
        presenter.$removeConfirmationBoxStopwatch = null;
        presenter.$bottomPanels = null;

        presenter.config = null;

        presenter.iwb_tmp_canvas = null;
        presenter.tmp_ctx = null;

        presenter.$markerMask = null;
        presenter.$markerCanvas = null;
        presenter.markerCtx = null;
        presenter.$penMask = null;
        presenter.ctx = null;
        presenter.$canvas = null;
        presenter.$selectingMask = null;
        presenter.selectingCtx = null;
        presenter.selectingCanvas = null;

        /***
         * *****************************************
         * presenter.addFloatingImages - kinetic.js
         * *****************************************
         */
        presenter.$floatingImageMask.off();

        if(presenter.floatingImageLayer) {
            presenter.floatingImageLayer.destroy();
        }
        if(presenter.floatingImageStage) {
            presenter.floatingImageStage.destroy();
        }

        for(var i = 0; i &lt; 3; i++) {
            if(presenter._kinetic.images[i]) {
                presenter._kinetic.images[i].destroy();
            }

            //imageRotateObj
            if(presenter._kinetic.rotateObj[i]) {
                $(presenter._kinetic.rotateObj[i]).off();
                presenter._kinetic.rotateObj[i].src = '';
            }

            //imageMoveObj
            if(presenter._kinetic.moveObj[i]) {
                $(presenter._kinetic.moveObj[i]).off();
                presenter._kinetic.moveObj[i].src = '';
            }


            //imageObj
            if(presenter._kinetic.imageObj[i]) {
                $(presenter._kinetic.imageObj[i]).off();
                presenter._kinetic.imageObj[i].src = '';
            }

            if(presenter._kinetic.rotateIcon[i]) {
                presenter._kinetic.rotateIcon[i].destroy();
            }
            if(presenter._kinetic.moveIcon[i]) {
                presenter._kinetic.moveIcon[i].destroy();
            }
            if(presenter.floatingImageGroups[i]) {
                presenter.floatingImageGroups[i].destroy();
            }
        }

        presenter._kinetic.images = [];
        presenter._kinetic.rotateObj = [];
        presenter._kinetic.moveObj = [];
        presenter._kinetic.rotateIcon = [];
        presenter._kinetic.moveIcon = [];

        presenter._kinetic.groups = null;
        presenter._kinetic.images = null;
        presenter._kinetic.rotateObj = null;
        presenter._kinetic.moveObj = null;
        presenter._kinetic.rotateIcon = null;
        presenter._kinetic.moveIcon = null;

        presenter._kinetic.imageObj = [];
        presenter._kinetic.imageObj.length = 0;
        presenter._kinetic.imageObj = null;

        presenter._kinetic = null;

        presenter.floatingImageLayer = null;
        presenter.floatingImageStage = null;
        presenter.allImagesLoadedPromises = [];
        presenter.allImagesLoadedPromises.length = 0;
        presenter.allImagesLoadedPromises = null;

        /***
         * ***********************************************
         *  applyHovered
         * ***********************************************
         */
        presenter._hoveredButtons.forEach(function ($btn) {
            $btn.off();
        });
        presenter._hoveredButtons.length = [];
        presenter._hoveredButtons = null;

        /***
         * **************************************************
         * addScrollHandler
         * **************************************************
         */
         if (presenter.playerController &amp;&amp; !presenter.playerController.isPlayerInCrossDomain()) {
             $(window.parent.document).unbind('scroll', IWB_Toolbar_scrollHandler);
             $(window.parent.document).find('#lesson-view &gt; div &gt; div').unbind('scroll', IWB_Toolbar_scrollHandler);
             $(window).unbind('scroll', IWB_Toolbar_scrollHandler);
             $("#content-view").unbind('scroll', IWB_Toolbar_scrollHandler);
         }

        /***
         * ************************************************
         * zoom
         * ************************************************
         */
        if(zoom) {
            zoom.destroy();
        }

        /***
         *************************************************
         * setState
         *************************************************
        */
        presenter._setState.images.forEach(function (image) {
            $(image).off();
            image.src = '';
        });
        presenter._setState.images.length = 0;
        presenter._setState.images = null;

        presenter._setState = null;



        /***
         * ***********************************************
         * functions
         * ***********************************************
         */
        presenter.setBasicConfiguration = null;
        presenter.changeCursor = null;
        presenter.createCanvases = null;
        presenter.createCanvas = null;
        presenter.addFloatingImages = null;
        presenter.loadDeferredFloatingImage = null;
        presenter.applyHovered = null;
        presenter.setVisibility = null;
        presenter.openPanel = null;
        presenter._openPanelShow = null;
        presenter._closePanelShow = null;
        presenter.toogleMasks = null;
        presenter.isSupportCSSPointerEvents = null;
        presenter.panelView = null;
        presenter.markerDrawingLogic = null;
        applyOnDblClickHandler = null;
        applyDoubleTapHandler = null;

        //buttonsLogic clearing
        presenter.addEventHandlers = null;
        presenter.penClickHandler = null;
        presenter.penCloseHandler = null;
        presenter.penUnclickHandler = null;
        presenter.markerClickHandler = null;
        presenter.markerCloseHandler = null;
        presenter.markerUnclickHandler = null;
        presenter.defaultClickHandler = null;
        presenter.colorClickHandler = null;
        presenter.thicknessClickHandler = null;
        presenter.zoomClickHandler = null;
        presenter.zoomCloseHandler = null;
        presenter.eraserClickHandler = null;
        presenter.eraserUnClickHandler = null;
        presenter.hideAreaClickHandler = null;
        presenter.standAreaClickHandler = null;
        presenter.resetClickHandler = null;
        presenter.resetOneClickHandler = null;
        presenter.redoOneClickHandler = null;
        presenter.noteClickHandler = null;
        presenter.customScriptClickHandler = null;
        presenter.floatingImageClickHandler = null;
        presenter.clockClickHandler = null;
        presenter.stopwatchClickHandler = null;
        presenter.closeClickHandler = null;
        presenter.openClickHandler = null;
        presenter.areDrawingButtonsActive = null;
        presenter.isDrawingActive = null;
        presenter.isAreaDrawingActive = null;
        presenter.getTheWidestAndHighest = null;
        presenter.preventClickAction_zoomClickHandler = null;
        presenter.zoomSelectedModule = null;
        presenter.drawAreaLogic = null;
        presenter.setImagePosition = null;
        presenter.createClock = null;
        presenter.createStopwatch = null;
        presenter.closePanel = null;

        //markerDrawingLogicFunctions
        presenter.onTouchStartCallback = null;
        presenter.onTouchEndEventCallback = null;
        presenter.markerPointerMoveHandler = null;
        presenter.markerPointerDownHandler = null;
        presenter.markerPointerUpHandler = null;

        //penDrawingLogicFunctions
        presenter.penPointerMoveHandler = null;
        presenter.penPointerDownHandler = null;
        presenter.penPointerUpHandler = null;

        //hideArea
        presenter.drawAreaLogic_touchStartCallback = null;
        presenter.drawAreaLogic_touchEndCallback = null;
        presenter.drawAreaLogic_mouseDownCallback = null;
        presenter.drawAreaLogic_mouseUpCallback = null;

        presenter.drawAreaLogic = null;

        presenter.disableTextAudioEventHandlers = null;
        presenter.restoreTextAudioEventHandlers = null;
        presenter.changeColor = null;
        presenter.changeThickness = null;
        presenter.drawingLogic = null;
        presenter.toggleBottomPanels = null;
        presenter.reset = null;
        presenter._reset = null;
    };

    presenter._reset = function IWB_Toolbar_private_reset(closePanel, shouldClearCanvas, shouldHideDrawingMasks, shouldHideSelectingMasks, shouldHideFloatingImage, shouldClearSelectingMask) {
        presenter.$panel.find('.clicked-lighter').removeClass('clicked-lighter');
        presenter.$panel.find('.hovered').removeClass('hovered');
        presenter.$pagePanel.find('.zoomed').removeClass('zoomed');
        presenter.$pagePanel.enableSelection();
        presenter.changeCursor('default');

        if (closePanel) {
            presenter.$pagePanel.find('.bottom-panel-color').hide();
            presenter.$pagePanel.find('.bottom-panel-thickness').hide();
        }

        if (shouldClearCanvas) {
            presenter.changeColor('#0fa9f0');
            clearCanvases();
        }

        if (shouldHideDrawingMasks &amp;&amp; !presenter.isSupportCSSPointerEvents()) {
            if (presenter.$penMask) {
                presenter.$penMask.hide();
            }
            if (presenter.$markerMask) {
                presenter.$markerMask.hide();
            }
        }

        if (shouldHideSelectingMasks) {
            if (presenter.$selectingMask) {
                presenter.$selectingMask.hide();
            }
        }

        if(shouldHideFloatingImage){
            if (presenter.$floatingImageMask) {
                presenter.$floatingImageMask.hide();
                presenter.$pagePanel.find('.bottom-panel-floating-image').hide();
                presenter.$pagePanel.find('.bottom-panel-floating-image').attr('isHidden', '1');
            }
        }

        if (shouldClearSelectingMask) {
            clearSelectingCanvas();
        }

        setOverflowWorkAround(true);
        setOverflowWorkAround(false);
    };

    presenter.changeColor = function IWB_Toolbar_changeColor(color, button) {
        if (button) {
            presenter.$panel.find('.button.color').css('background-image', $(button).css('background-image'));
        } else {
            presenter.$panel.find('.button.color').css('background-image', presenter.$defaultColorButton.css('background-image'));
        }
        presenter.currentLineColor = color;
    };

    presenter.changeThickness = function IWB_Toolbar_changeThickness(size, button) {
        if (button) {
            presenter.$panel.find('.button.thickness').css('background-image', $(button).css('background-image'));
        } else {
            presenter.$panel.find('.button.thickness').css('background-image', presenter.$defaultThicknessButton.css('background-image'));
        }
        presenter.currentLineWidth = presenter.data.defaultPenWidth === 1 ? size : presenter.data.defaultPenWidth;

        presenter.currentMarkerThickness = presenter.data.markerThickness === 10 ? size : presenter.data.markerThickness;
        presenter.currentEraserThickness = presenter.data.eraserThickness === 20 ? size : presenter.data.eraserThickness;
    };

    function clearCanvases() {
        if (presenter.$canvas) {
            presenter.penUsed = false;
            presenter.$canvas.off(`${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.UP}`);
            presenter.ctx.clearRect(0, 0, presenter.$canvas[0].width, presenter.$canvas[0].height);
        }

        if (presenter.$markerCanvas) {
            presenter.markerUsed = false;
            presenter.$markerCanvas.off(`${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.UP}`);
            presenter.markerCtx.clearRect(0, 0, presenter.$markerCanvas[0].width, presenter.$markerCanvas[0].height);
        }
    }

    function clearSelectingCanvas() {
        if (presenter.selectingCanvas) {
            presenter.selectingCanvas.off(`${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.UP}`);
            presenter.selectingCtx.clearRect(0, 0, presenter.selectingCanvas[0].width, presenter.selectingCanvas[0].height);
        }
    }

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$panel.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.isCustomButtonActive = function() {
        return presenter.customButtonActive;
    }

    presenter.executeCommand = function(name, params) {
        var commands = {
            'deselectCustomButton': presenter.deselectCustomButton,
            'open' : presenter.open,
            'hide' : presenter.hide,
            'show' : presenter.show,
            'setPenColor' : presenter.setPenColor,
            'setMarkerColor' : presenter.setMarkerColor,
            'setDefaultPenThickness' : presenter.setDefaultPenThickness,
            'setMarkerThickness': presenter.setMarkerThickness,
            'setEraserThickness': presenter.setEraserThickness
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.reset = function IWB_Toolbar_reset() {
        if (presenter.customButtonActive) {
            presenter.deselectCustomButton();
        }
        presenter.$pagePanel.find('.clicked').removeClass('clicked');
        presenter._reset(true, true, true, true, true, true);
        presenter.penLineWidth = 1;
        presenter.data.defaultPenWidth = 1;
        presenter.markerLineWidth = 10;
        presenter.data.eraserThickness = 20;
        presenter.penDataURL = null;
        presenter.markerDataUrl = null;
    };

    presenter.getErrorCount = function() { return 0; };
    presenter.getMaxScore = function() { return 0; };
    presenter.getScore = function() { return 0; };

    function getSavedClocks() {
        var clocks = [];
        $.each(presenter.$pagePanel.find('.iwb-toolbar-clock'), function() {
            clocks.push({
                'top': $(this).css('top'),
                'left': $(this).css('left')
            });
        });
        return clocks;
    }

    function getSavedStopwatches() {
        var stopwatches = [];
        $.each(presenter.$pagePanel.find('.iwb-toolbar-stopwatch'), function() {
            stopwatches.push({
                'top': $(this).css('top'),
                'left': $(this).css('left')
            });
        });
        return stopwatches;
    }

    function isPanelOpened() {
        return presenter.$panel.hasClass('opened');
    }

    presenter.getState = function() {
        zoom.out();
        var notes = presenter.noteObjects.map(function (note) {
            return note.getState();
        });

        var clocks = getSavedClocks(),
           stopwatches = getSavedStopwatches(),
           position = presenter.$panel.position(),
           openedPanel = isPanelOpened(),
           drawings = {
               'pen' : (presenter.penUsed &amp;&amp; presenter.$canvas) ? presenter.penDataURL : null,
               'marker' : (presenter.markerUsed &amp;&amp; presenter.$markerCanvas) ? presenter.markerDataUrl : null
           };

        var stateColor;
        var stateThickness;
        if(openedPanel){
           if(presenter.shouldSaveColor == 'pen' || presenter.shouldSaveColor == 'stand-area' || presenter.shouldSaveColor == 'hide-area'){
               presenter.closePenColor = presenter.currentLineColor;
               presenter.closePenThickness = presenter.currentLineWidth;

               presenter.isCloseColor = true;
           }else if(presenter.shouldSaveColor == 'marker'){
               presenter.closePenColor = presenter.currentLineColor;
               presenter.closePenThickness = presenter.currentMarkerThickness;

               presenter.isCloseColor = true;
           }
           else{
               presenter.isCloseColor = false;
           }
           if(presenter.activeButton != 'open'){
               presenter.activeFunction = presenter.activeButton;
           }
        }

        if(presenter.shouldSaveColor == 'pen' || presenter.shouldSaveColor == 'stand-area' || presenter.shouldSaveColor == 'hide-area'){
           stateColor = presenter.closePenColor;
           stateThickness = presenter.closePenThickness;
        }else if(presenter.shouldSaveColor == 'marker'){
           stateColor = presenter.closePenColor;
           stateThickness = presenter.closePenThickness;
        }else{
           stateColor = '';
           stateThickness = '';
        }

        if(presenter.isKeepStateAndPosition){
           if(window.savedPanel.tools){
               if(!presenter.activeFunction || presenter.activeFunction == 'open' || presenter.activeFunction == 'close'){
                   presenter.activeFunction = window.savedPanel.tools.activeFunction;
               }
           }
        }

        var isSelectingVisible = presenter.$selectingMask.is(':visible');

        window.savedPanel.tools = {
           'activeFunction': presenter.activeFunction,
           'customButtonActive': presenter.customButtonActive,
           'stateColor': stateColor,
           'stateThickness': stateThickness,
           'isCloseColor': presenter.isCloseColor,
           'buttonColor': $(presenter.buttonColor).attr("color"),
           'buttonThickness': $(presenter.buttonThickness).attr("thickness"),
           'shouldSaveColor': presenter.shouldSaveColor
        };

        var state = {
           'areas' : presenter.areas,
           'notes' : notes,
           'clocks' : clocks,
           'stopwatches' : stopwatches,
           'drawings' : drawings,
           'seconds' : presenter.seconds,
           'minutes' : presenter.minutes,
           'hours' : presenter.hours,
           'stopClicked' : presenter.stopButtonClicked,
           'startClicked' : presenter.startButtonClicked,
           'isVisible' : presenter.isVisible,
           'position' : position,
           'openedPanel' : openedPanel,
           'activeFunction': presenter.activeFunction,
           'customButtonActive': presenter.customButtonActive,
           'stateColor': stateColor,
           'stateThickness': stateThickness,
           'isCloseColor': presenter.isCloseColor,
           'buttonColor': $(presenter.buttonColor).attr("color"),
           'buttonThickness': $(presenter.buttonThickness).attr("thickness"),
           'shouldSaveColor': presenter.shouldSaveColor,
           'isSelectingVisible': isSelectingVisible
        };

        return JSON.stringify(state);
    };

    /**
     * We are omitting state properties as follows:
     * - hours
     * - minutes
     * - seconds
     * - stopClicked
     * - startClicked
     *
     * Because they are used when creating stopwatches and we assume here that no stopwatches should be created
     * (hence empty array of them).
     */
    presenter.upgradeStateForStopwatchesAndClocks = function(parsedState) {
        if (parsedState.stopwatches == undefined) {
            parsedState.stopwatches = [];
        }
        if (parsedState.clocks == undefined) {
            parsedState.clocks = [];
        }

        return parsedState;
    };

    presenter.upgradeStateForVisibility = function (parsedState) {
        if (parsedState.isVisible == undefined) {
            parsedState.isVisible = true;
        }

        return parsedState;
    };

    presenter.upgradeStateForSavingTools = function (parsedState){
        if(parsedState.activeFunction == undefined){
            parsedState.activeFunction = '';
        }
        if(presenter.customButtonActive == undefined){
            parsedState.customButtonActive = '';
        }
        if(parsedState.buttonColor == undefined){
            parsedState.buttonColor = '';
        }
        if(parsedState.buttonThickness == undefined){
            parsedState.buttonThickness = '';
        }
        if(parsedState.isCloseColor == undefined){
            parsedState.isCloseColor = false;
        }
        if(parsedState.shouldSaveColor == undefined){
            parsedState.shouldSaveColor = '';
        }
        if(parsedState.stateColor == undefined){
            parsedState.stateColor = '#000';
        }
        if(parsedState.stateThickness == undefined){
            parsedState.stateThickness = 1;
        }

        return parsedState;
    };

    presenter.upgradeStateForSelectingMaskVisibility = function (parsedState) {
            if (parsedState.isSelectingVisible == undefined) {
                parsedState.isSelectingVisible = false;
            }

            return parsedState;
        };

    presenter.upgradeState = function (parsedState) {
        var upgradedState = presenter.upgradeStateForStopwatchesAndClocks(parsedState);
        upgradedState = presenter.upgradeStateForVisibility(upgradedState);
        upgradedState = presenter.upgradeStateForSavingTools(upgradedState);
        upgradedState = presenter.upgradeStateForSelectingMaskVisibility(upgradedState);

        return  upgradedState;
    };

    presenter.shouldRestoreStateAndPosition = function (model, state) {
        var keepStateAndPosition = model['keepStateAndPosition'];

        if (keepStateAndPosition == undefined || state.position == undefined) {
            return false;
        }

        return keepStateAndPosition === 'True';
    };

    presenter.setState = function(state) {
        if (!state) {
           return;
        }
        var parsedState = JSON.parse(state);

        var upgradedState = presenter.upgradeState(parsedState);
        presenter.areas = parsedState.areas;
        presenter.stopwatches = parsedState.stopwatches;
        presenter.clocks = parsedState.clocks;
        if (presenter.shouldRestoreStateAndPosition(presenter.model, upgradedState)) {
           if (upgradedState.openedPanel) {
               if(presenter.isKeepStateAndPosition){
                   presenter.openPanel(false);
               }
           }
           presenter.position = upgradedState.position;

           var visibility = presenter.$panel.css('visibility');
           var width = presenter.$panel.css('width');
           var position = presenter.$panel.css('position');

           presenter.$panel.attr('style', 'position: ' + position + '; top: ' + presenter.position.top + 'px; left: ' + presenter.position.left + 'px; visibility: ' + visibility + '; width: ' + width + ';');
        }


        var image1 = new Image();
        var image2 = new Image();
        presenter._setState.images.push(image1);
        presenter._setState.images.push(image2);

        if (upgradedState.drawings.pen) {
            presenter.penUsed = true;
        }
        if (upgradedState.drawings.marker) {
            presenter.markerUsed = true;
        }

        presenter.penDataURL = upgradedState.drawings.pen;
        presenter.markerDataUrl = upgradedState.drawings.marker;
        setDrawingState(new Image(), presenter.ctx, upgradedState.drawings.pen);
        setDrawingState(new Image(), presenter.markerCtx, upgradedState.drawings.marker);

        $.each(parsedState.notes, function(_, noteData) {
            var note = presenter.createNote(noteData);
            presenter.noteObjects.push(note);
            presenter.$pagePanel.find('.ic_page').append(note.getView());
        });

        $.each(presenter.clocks, function() {
           presenter.createClock(this);
        });

        $.each(presenter.stopwatches, function() {
           presenter.createStopwatch(this, upgradedState.hours, upgradedState.minutes, upgradedState.seconds, upgradedState.stopClicked, upgradedState.startClicked);
        });

        drawSavedAreas();
        presenter.isVisible = upgradedState.isVisible;
        presenter.setVisibility(presenter.isVisible, false, presenter.$view);

        if (presenter.isSupportCSSPointerEvents()) {
           presenter.$penMask.show();
           presenter.$markerMask.show();
           if (presenter.isKeepStateAndPosition
               &amp;&amp; window.savedPanel.tools != undefined
               &amp;&amp; (window.savedPanel.tools.activeFunction == 'pen' || window.savedPanel.tools.activeFunction == 'marker')
               &amp;&amp; window.savedPanel.isOpen)
           {
               presenter.$penMask.css('pointer-events', 'auto');
               presenter.$markerMask.css('pointer-events', 'auto');
               presenter.$penMask.css('touch-action', 'none');
               presenter.$markerMask.css('touch-action', 'none');
           } else {
               presenter.$penMask.css('pointer-events', 'none');
               presenter.$markerMask.css('pointer-events', 'none');
               presenter.$penMask.css('touch-action', 'auto');
               presenter.$markerMask.css('touch-action', 'auto');
           }
        }

        if(presenter.isKeepStateAndPosition &amp;&amp; (window.savedPanel.tools != undefined)){
           presenter.customButtonActive = window.savedPanel.tools.customButtonActive;
           presenter.activeFunction = window.savedPanel.tools.activeFunction;
           presenter.closePenColor = window.savedPanel.tools.stateColor;
           presenter.closePenThickness = window.savedPanel.tools.stateThickness;
           presenter.buttonColor = presenter.$bottomPanels.find('[color*='+window.savedPanel.tools.buttonColor+']')[0];
           presenter.buttonThickness = presenter.$bottomPanels.find('[thickness*='+window.savedPanel.tools.buttonThickness+']')[0];
           presenter.isCloseColor = window.savedPanel.tools.isCloseColor;
           presenter.shouldSaveColor = window.savedPanel.tools.shouldSaveColor;
        }else{
           presenter.customButtonActive = upgradedState.customButtonActive;
           presenter.activeFunction = upgradedState.activeFunction;
           presenter.closePenColor = upgradedState.stateColor;
           presenter.closePenThickness = upgradedState.stateThickness;
           presenter.buttonColor = presenter.$bottomPanels.find('[color*='+upgradedState.buttonColor+']')[0];
           presenter.buttonThickness = presenter.$bottomPanels.find('[thickness*='+upgradedState.buttonThickness+']')[0];
           presenter.isCloseColor = upgradedState.isCloseColor;
           presenter.shouldSaveColor = upgradedState.shouldSaveColor;
        }
        if(presenter.activeFunction){
           presenter.activeButton = presenter.activeFunction;
        }

        presenter.isSavedState = true;

        if(!presenter.isKeepStateAndPosition){
           if(presenter.isCloseColor){
               presenter.changeColor(presenter.closePenColor, presenter.buttonColor);
               presenter.changeThickness(presenter.closePenThickness, presenter.buttonThickness);
           }
        }

        if(upgradedState.isSelectingVisible) {
            presenter.$selectingMask.show();
        } else {
            presenter.$selectingMask.hide();
        }

        setOverflowWorkAround(true);
        setOverflowWorkAround(false);
    };

    presenter.updateZoomConfiguration = function () {
        presenter.zoomConfiguration.playerInitialLeftOffset = $("#_icplayer").offset().left;
        presenter.zoomConfiguration.viewLeftOffset = presenter.$panel.offset().left;
    };

    function setDrawingState(image, ctx, data) {
        if (data) {
            $(image).load(function() {
                ctx.drawImage(image, 0, 0);
            });
            image.src = data;
        }
    }

    presenter.setPenColor = function(color) {
        color = color[0];
        presenter.data.penColor = color;
        if (presenter.$pagePanel.find('.clicked').hasClass('pen')) {
            presenter.currentLineColor = color;
        }
    };

    presenter.setMarkerColor = function(color) {
        color = color[0];
        presenter.data.markerColor = color;
        if (presenter.$pagePanel.find('.clicked').hasClass('marker')) {
            presenter.currentLineColor = color;
        }
    };

    presenter.setDefaultPenThickness = function(lineWidth) {
        presenter.data.defaultPenWidth = parseInt(lineWidth, 10);
        presenter.changeThickness(presenter.data.defaultPenWidth);
    };

    presenter.setMarkerThickness = function (thickness){
        presenter.data.markerThickness = parseInt(thickness, 10);
        presenter.changeThickness(presenter.data.markerThickness);
    };

    presenter.setEraserThickness = function (thickness){
        presenter.data.eraserThickness = parseInt(thickness, 10);
        presenter.changeThickness(presenter.data.eraserThickness);
    };

    presenter.destroyDraggableItems = function() {
        for (var i = 0; i &lt; presenter.noteObjects.length; i++) {
            presenter.noteObjects[i].destroy();
        }
        presenter.noteObjects = [];

        if (presenter.stopwatchAdded) {
            closeStopwatch();
        }

        if (presenter.clockAdded) {
            closeClock();
        }
    }

    presenter.resetDrawingMode = function() {
        presenter.drawMode = presenter.DRAW_MODE.NONE;
        presenter.ctx.globalCompositeOperation = 'source-over';
        presenter.markerCtx.globalCompositeOperation = 'source-over';
        presenter.$penMask.css('pointer-events', 'none');
        presenter.$markerMask.css('pointer-events', 'none');
        presenter.$penMask.css('touch-action', 'auto');
        presenter.$markerMask.css('touch-action', 'auto');
    }

    presenter.restoreLastState = function() {
        presenter.stateStack.restoreLastState();
    }

    presenter.pushStateToStack = function() {
        this.stateStack.pushStateToStack();
    }

    presenter.redoStateFromArray = function() {
        presenter.stateStack.redoStateFromArray();
    }

    // Method added to aid in testing
    presenter.getStateStack = function() {
        if (presenter.stateStack == null) {
            presenter.stateStack = new StateStack(presenter);
        }
        return presenter.stateStack;
    }

    function StateStack(iwbPresenter) {
        this.iwbPresenter = iwbPresenter;
        this.currentState = "";
        this.prevStateStack = [];
        this.nextStateStack = [];
        this.stateStackOverflow = false;
        if (iwbPresenter.playerController) {
            var pageIndex = iwbPresenter.playerController.getCurrentPageIndex();
            if (window.savedPanel &amp;&amp; window.savedPanel.histories &amp;&amp; window.savedPanel.histories[pageIndex]) {
                this.setState(window.savedPanel.histories[pageIndex]);
            }
        }
    }

    StateStack.prototype.getState = function() {
        var state = {
            currentState: this.currentState,
            prevStateStack: this.prevStateStack,
            nextStateStack: this.nextStateStack,
            stateStackOverflow: this.stateStackOverflow
        };
        return JSON.stringify(state);
    }

    StateStack.prototype.setState = function(state) {
        var parsedState = JSON.parse(state);
        this.currentState = parsedState.currentState;
        this.prevStateStack = parsedState.prevStateStack;
        this.nextStateStack = parsedState.nextStateStack;
        this.stateStackOverflow = parsedState.stateStackOverflow;
    }

    StateStack.prototype.compareStates = function(state1, state2) {
        if (state1.length == 0 || state2.length == 0) {
            return state1.length == state2.length;
        }
        var json1 = JSON.parse(state1);
        var json2 = JSON.parse(state2);
        var newJson1 = {};
        var newJson2 = {};
        var compareFields = ["areas", "clocks", "drawings", "notes", "stopwatches"];
        for(var i = 0; i &lt; compareFields.length; i++) {
            var fieldName = compareFields[i];
            newJson1[fieldName] = json1[fieldName];
            newJson2[fieldName] = json2[fieldName] ;
        }
        var result = JSON.stringify(newJson1) == JSON.stringify(newJson2);
        return result;
    }

    StateStack.prototype.pushStateToStack = function() {
        if (!this.iwbPresenter.config.enableUndoRedo) return;
        var newState = this.iwbPresenter.getState();
        if (!this.compareStates(newState, this.currentState)) {
            if (this.nextStateStack.length &gt; 0) {
                this.nextStateStack = [];
            }
            if (this.currentState.length &gt; 0) {
                this.prevStateStack.push(this.currentState);
                this.currentState = newState;
                if (this.prevStateStack.length &gt; 5) {
                    this.stateStackOverflow = true;
                    this.prevStateStack.splice(0, this.prevStateStack.length - 5);
                }
            } else {
                this.currentState = newState;
            }
            this.updateGlobalHistory();
        }
    }

    StateStack.prototype.restoreLastState = function() {
        if (!this.iwbPresenter.config.enableUndoRedo) return;
        this.iwbPresenter.resetDrawingMode();
        if (this.prevStateStack.length &gt; 0) {
            this.iwbPresenter.reset();
            this.iwbPresenter.destroyDraggableItems();
            this.nextStateStack.push(this.currentState);
            this.currentState = this.prevStateStack.pop();
            this.iwbPresenter.setState(this.currentState);
            this.updateGlobalHistory();
        } else if (!this.stateStackOverflow &amp;&amp; this.currentState.length &gt; 0) {
            this.iwbPresenter.reset();
            this.iwbPresenter.destroyDraggableItems();
            this.nextStateStack.push(this.currentState);
            this.currentState = "";
            this.updateGlobalHistory();
        }
    }

    StateStack.prototype.redoStateFromArray = function() {
        if (!this.iwbPresenter.config.enableUndoRedo) return;
        this.iwbPresenter.resetDrawingMode();
        if (this.nextStateStack.length &gt; 0) {
            this.iwbPresenter.reset();
            this.iwbPresenter.destroyDraggableItems();
            this.prevStateStack.push(this.currentState);
            this.currentState = this.nextStateStack.pop();
            this.iwbPresenter.setState(this.currentState);
            this.updateGlobalHistory();
        }
    }

    StateStack.prototype.updateGlobalHistory = function() {
        if (!window.savedPanel.histories) {
            window.savedPanel.histories = {};
        };
        if (presenter.playerController) {
            var pageIndex = this.iwbPresenter.playerController.getCurrentPageIndex();
            window.savedPanel.histories[pageIndex] = this.getState();
        }
    }

    return presenter;
}

AddonIWB_Toolbar_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Iframe" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Iframe URL" name="iframeURL" nameLabel="IFrame_property_iframe_url" type="string"/>
        <property displayName="File List" name="fileList" nameLabel="IFrame_property_file_list" type="list">
            <property displayName="File" name="file" nameLabel="IFrame_property_file" type="file"/>
            <property displayName="ID" name="id" nameLabel="IFrame_property_id" type="string"/>
        </property>
        <property displayName="Index Html Location" name="indexHtmlLocation" nameLabel="IFrame_property_index_html_location" type="string"/>
        <property displayName="Zip File" name="zipFile" nameLabel="IFrame_property_zip_file" type="file"/>
        <property displayName="Index File" name="index" nameLabel="IFrame_property_index_file" type="file"/>
        <property displayName="Communication ID" name="communicationID" nameLabel="IFrame_property_communication_id" type="string"/>
        <property name="allowFullscreen" nameLabel="IFrame_property_allow_fullscreen" type="boolean"/>
        <property isLocalized="true" name="Alt text" nameLabel="IFrame_property_alt_text" type="string"/>
    </model>
<css>.addon-IFrame{
    width: 100%;
    height: 100%;
    border: none;
    position: absolute;
}
</css><view>&lt;div class="addon-IFrame-wrapper"&gt;
    &lt;iframe class="addon-IFrame"&gt;
    &lt;/iframe&gt;
&lt;/div&gt;
</view><preview>&lt;div class="addon-IFrame-wrapper"&gt;
    &lt;iframe class="addon-IFrame"&gt;
    &lt;/iframe&gt;
&lt;/div&gt;
</preview><presenter>/**
 * 
 * KNOWN WORKAROUNDS:
 *  Iframe src:
 *      -mCourser have optimalization on /file/serve, where file serving is redirecting to GCS. Files passed in FILE_DICTIONARY_ACTUALIZATION are relative to domain, so after redirect that files while request are built by browser as storage.google.com/file/serve/&lt;id&gt;. To fix it add no_gcs flag to address
 * 
 */
function AddonIframe_create() {
    var presenter = function (){};

    presenter.iframeState = undefined;
    presenter.iframeContent = null;
    presenter.configuration = null;
    presenter.$view = null;
    presenter.eventBus = null;
    presenter.isEditor = false;
    presenter.isVisible = true;
    presenter.originalDisplay = "block";

    presenter.actionID = {
        SET_WORK_MODE : "SET_WORK_MODE",
        SET_SHOW_ERRORS_MODE : "SET_SHOW_ERRORS_MODE",
        RESET : "RESET",
        STATE_ACTUALIZATION: "STATE_ACTUALIZATION",
        STATE_REQUEST: "STATE_REQUEST",
        SHOW_ANSWERS: "SHOW_ANSWERS",
        HIDE_ANSWERS: "HIDE_ANSWERS",
        FILE_DICTIONARY_REQUEST: "FILE_DICTIONARY_REQUEST",
        FILE_DICTIONARY_ACTUALIZATION: "FILE_DICTIONARY_ACTUALIZATION",
        CUSTOM_EVENT: "CUSTOM_EVENT"
    };

    presenter.iframeScore = {
            pageCount: 0,
            checks: 0,
            errors: 0,
            mistakes: 0,
            score: 0,
            maxScore: 0,
            scaledScore: 0
    };

    presenter.ERROR_CODES = {
        'M01' : "Module must have Index File or IFrame URL",
        'M02' : "Module must have Communication ID",
        'F01' : "File must have ID",
        'F02' : "In File List all elements must have file",
        'I01' : "Multiple File ID in File List"
    };

    presenter.validateMessage = function AddonIFrame_Communication_Validate_Message (message) {
        if (message != undefined) {
            if ((message.id == presenter.configuration.communicationID) &amp;&amp; (message.actionID != undefined)) {
                return true;
            }
        }
        return false;
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.run = function AddonIFrame_Communication_run (view, model) {
        presenter.initialize(view, model);
        if (presenter.configuration.isValid) {
            presenter.setVisibility(presenter.configuration.isVisibleByDefault);
            presenter.eventBus.addEventListener('ShowAnswers', this);
            presenter.eventBus.addEventListener('HideAnswers', this);
        }
    };

    presenter.createPreview = function AddonIFrame_Communication_create_preview (view, model) {
        presenter.isEditor = true;
        presenter.initialize(view, model);
        if (presenter.configuration.isValid) {
            presenter.setVisibility(true);
        }
    };

    presenter.getIframeIndexSource = function () {
        var source = presenter.configuration.index;
        if (source.indexOf("/file/serve") &gt; -1) {
            var separator = (presenter.configuration.index.indexOf("?")===-1)?"?":"&amp;";
            source = presenter.configuration.index + separator + "no_gcs=true";
        }

        return source;
    };

    presenter.initialize = function AddonIFrame_Communication_initialize (view, model)  {
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        window.addEventListener("message", presenter.getMessage, false);

        var iframe = $(view).find("iframe");

        if (presenter.configuration.allowFullScreen) {  // It must be done before src setup.
            iframe.attr("allowfullscreen", "allowfullscreen");
            iframe.attr("webkitallowfullscreen", "webkitallowfullscreen");
            iframe.attr("mozallowfullscreen", "mozallowfullscreen");
        }

        if(presenter.configuration.haveURL) {
            iframe.attr("src", presenter.configuration.iframeURL);
        }
        else {
            iframe.attr("src", presenter.getIframeIndexSource());
        }

        presenter.$view = $(view);
        presenter.view = view;

         var display = presenter.$view.css('display');
        if (display != null &amp;&amp; display.length &gt; 0) {
            presenter.originalDisplay = display;
        }

        presenter.iframeContent = iframe.get(0).contentWindow;

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        presenter.$view.attr('alt', presenter.configuration.altText);
    };

    presenter.destroy = function () {
        window.removeEventListener("message",presenter.getMessage);
    };

    presenter.validateFile = function AddonIFrame_Communication_Validate_File (file, dictionary) {
        var trimmedFileID = file.id.trim();
        var trimmedFile = file.file.trim();

        if (ModelValidationUtils.isStringEmpty(trimmedFileID)) {
            return {isValid: false, errorCode: "F01" };
        }

        if (ModelValidationUtils.isStringEmpty(trimmedFile)) {
            return {isValid: false, errorCode: "F02" };
        }

        if (dictionary[trimmedFileID] !== undefined) {
            return {isValid: false, errorCode: "I01" };
        }

        return {isValid: true, id: trimmedFileID, file: trimmedFile};
    };


    presenter.validateFileList = function AddonIFrame_Communication_Validate_File_List (model) {
        var fileDictionary = {};

        for (var i = 0; i &lt; model.fileList.length; i++) {
            if(ModelValidationUtils.isStringEmpty(model.fileList[i].fileDictionary)
                &amp;&amp; ModelValidationUtils.isStringEmpty(model.fileList[i].file)){
                continue;
            }
            var validateFileResult = presenter.validateFile(model.fileList[i],fileDictionary);
            if (!validateFileResult.isValid) {
                return validateFileResult;
            }
            else {
                fileDictionary[validateFileResult.id] = validateFileResult.file;
            }
        }
        return {isValid: true, fileDictionary: fileDictionary};
    };
    presenter.validateIFrameSource = function AddonIFrame_Communication_Validate_IFrame_Source (model) {
        var haveURL = !ModelValidationUtils.isStringEmpty(model.iframeURL.trim());

        if (!haveURL &amp;&amp; ModelValidationUtils.isStringEmpty(model.index.trim()))  {
            return { isValid: false, errorCode: 'M01'};
        }
        return { isValid: true, haveURL: haveURL };
    };

    presenter.validateCommunicationID = function AddonIFrame_Communication_Validate_Communication_ID (model) {
       if (ModelValidationUtils.isStringEmpty(model.communicationID.trim()) ) {
           return { isValid: false, errorCode: 'M02'};
        }
        return { isValid: true, value: (model.communicationID.trim()) };
    };

    presenter.validateModel = function AddonIFrame_Communication_Validate_Model(model) {
        var validateIFrameSourceResult = presenter.validateIFrameSource(model);
        if (!validateIFrameSourceResult.isValid) {
            return validateIFrameSourceResult;
        }

        var validateCommunicationIDResult = presenter.validateCommunicationID(model);
        if (!validateCommunicationIDResult.isValid) {
            return validateCommunicationIDResult;
        }

        var validateFileListResult = presenter.validateFileList(model);
        if (!validateFileListResult.isValid) {
            return validateFileListResult;
        }

        var allowFullScreen = model['allowFullscreen'];
        if (allowFullScreen === undefined) {
            allowFullScreen = "False";
        }

        var altText = model['Alt text'];
        if(altText === undefined) {
            altText = '';
        }

        return {
            isValid: true,
            haveURL: validateIFrameSourceResult.haveURL,
            iframeURL: model.iframeURL,
            index: model.index,
            indexHtmlLocation: model.indexHtmlLocation,
            zipFile: model.zipFile,
            communicationID: validateCommunicationIDResult.value,
            addonID : model.ID,
            fileDictionary: validateFileListResult.fileDictionary,
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model['Is Visible']),
            allowFullScreen: ModelValidationUtils.validateBoolean(allowFullScreen),
            altText: altText
        };
    };

    presenter.setVisibility = function (isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
        if(!presenter.isEditor) {
            presenter.$view.css('display', isVisible ? presenter.originalDisplay : 'none');
        }
    };

    presenter.show = function AddonIFrame_Communication_show () {
        presenter.setVisibility(true);
    };

    presenter.hide = function AddonIFrame_Communication_hide () {
        presenter.setVisibility(false);
    };

    presenter.reset = function AddonIFrame_Communication_reset () {
        presenter.sendMessage(presenter.actionID.RESET);
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.isVisible = presenter.configuration.isVisibleByDefault;
    };

    presenter.showAnswers = function AddonIFrame_Communication_show_answers () {
        presenter.sendMessage(presenter.actionID.SHOW_ANSWERS);
    };

    presenter.hideAnswers = function AddonIFrame_Communication_hide_answers () {
        presenter.sendMessage(presenter.actionID.HIDE_ANSWERS);
    };

    presenter.setWorkMode = function AddonIFrame_Communication_set_work_mode () {
        presenter.sendMessage(presenter.actionID.SET_WORK_MODE);
    };

    presenter.setShowErrorsMode = function AddonIFrame_Communication_set_show_errors_mode () {
        presenter.sendMessage(presenter.actionID.SET_SHOW_ERRORS_MODE);
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.setState = function AddonIFrame_Communication_set_state (state) {
        try {
            var parsedState = JSON.parse(state);
            presenter.iframeState = parsedState.iframeState;
            presenter.iframeScore = parsedState.iframeScore;
            if(typeof(parsedState.isVisible) === "boolean") {
                presenter.isVisible = parsedState.isVisible;
            }else{
                presenter.isVisible = presenter.configuration.isVisibleByDefault;
            }
            presenter.setVisibility(presenter.isVisible);
        }
        catch (error) {
            presenter.iframeState = undefined;
        }
    };

    presenter.getState = function AddonIFrame_Communication_get_state () {
        return JSON.stringify({
            iframeState: presenter.iframeState,
            iframeScore: presenter.iframeScore,
            isVisible:presenter.isVisible,
        });
    };

    presenter.getScore = function AddonIFrame_Communication_get_score () {
        return presenter.iframeScore.score;
    };

    presenter.getMaxScore = function AddonIFrame_Communication_get_maxScore () {
        return presenter.iframeScore.maxScore;
    };

    presenter.getErrorCount = function AddonIFrame_Communication_get_error_count () {
        return presenter.iframeScore.errors;
    };

    presenter.sendMessageCommand = function (command) {
        presenter.sendMessage(command[0], command[1]);
    };

    presenter.executeCommand = function AddonIFrame_Communication_execute_Command (name, params) {
        if (presenter.isErrorCheckingMode) {
            return;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'sendMessage': presenter.sendMessage
        };
        Commands.dispatch(commands, name, params, presenter);
    };
    presenter.createEventData = function AddonIFrame_Communication_create_event_data() {
        return {
            source : presenter.configuration.addonID,
            item : "all",
            value : '',
            score : ''
        };

    };

   presenter.triggerFrameChangeEvent = function AddonIFrame_Communication_trigger_frame_change_event () {
        if (presenter.eventBus != undefined) {
            presenter.eventBus.sendEvent('ValueChanged', presenter.createEventData());

        }
    };

    presenter.createCustomEventData = function AddonIframe_Communication_create_custom_data (data) {
        return {
            source : presenter.configuration.addonID,
            item : "CUSTOM_EVENT",
            value : data.params,
            score : ''
        }
    };

    presenter.triggerCustomEvent = function AddonIFrame_Communication_trigget_custon_event (data) {
        if (presenter.eventBus != undefined) {
            presenter.eventBus.sendEvent('ValueChanged', presenter.createCustomEventData(data));

        }
    };

    presenter.sendMessage = function AddonIFrame_Communication_send_message (actionID, params) {
        if (params == undefined) {
            params = {};
        }
        var newMessage = { id : presenter.configuration.communicationID, actionID : actionID, params:params};
        presenter.iframeContent.postMessage(newMessage, "*");
    };

    presenter.setStateActualization = function AddonIFrame_Communication_set_state_actualization (state) {
        if (presenter.validateActualizationModel(state).isValid) {
            //state undefined nie aktualizwac
            if(state.iframeState !== undefined) {
                presenter.iframeState = state.iframeState;
            }
            presenter.iframeScore = state.iframeScore;
            if(typeof(presenter.isVisible) === "boolean") {
                presenter.isVisible = state.isVisible
            }else{
                presenter.isVisible = presenter.configuration.isVisibleByDefault;
            }
        }
    };

    presenter.validateActualizationModel = function AddonIFrame_Communication_Validate_Actualization_Model (actualization) {
        if (actualization === undefined) {
            return {isValid: false};
        }
        var attributes = ["pageCount", "checks", "errors", "mistakes", "score", "maxScore", "scaledScore"];
        if(actualization.iframeScore === undefined) {
            return {isValid: false };
        }
        for (var i = 0; i&lt; attributes.length; i++) {
            if (!ModelValidationUtils.validateInteger(actualization.iframeScore[attributes[i]]).isValid) {
                return {isValid: false };
            }
        }
        return {isValid: true};
    };

    presenter.getMessage = function AddonIFrame_Communication_get_message (event) {
        var message = event.data;
        if (presenter.validateMessage(message)) {
            switch (message.actionID) {
                case presenter.actionID.STATE_ACTUALIZATION:
                    presenter.setStateActualization(message.params);
                    presenter.triggerFrameChangeEvent();
                    break;

                case presenter.actionID.STATE_REQUEST:
                    presenter.sendMessage(presenter.actionID.STATE_ACTUALIZATION, { iframeState: presenter.iframeState, iframeScore: presenter.iframeScore } );
                    break;

                case presenter.actionID.FILE_DICTIONARY_REQUEST:
                    presenter.sendMessage(presenter.actionID.FILE_DICTIONARY_ACTUALIZATION, { fileDictionary: presenter.configuration.fileDictionary});
                    break;
                case presenter.actionID.CUSTOM_EVENT:
                    presenter.triggerCustomEvent(message);
                    break;
            }
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Image_Identification" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Image" nameLabel="Image_Identification_property_image" type="image"/>
		<property name="SelectionCorrect" nameLabel="Image_Identification_property_selection_correct" type="boolean"/>
		<property name="Is Disabled" nameLabel="Image_Identification_property_is_disabled" type="boolean"/>
		<property name="Is not an activity" nameLabel="Image_Identification_property_is_not_an_activity" type="boolean"/>
		<property name="Block in error checking mode" nameLabel="Image_Identification_property_block_in_error_checking_mode" type="boolean"/>
        <property displayName="Block wrong answers" name="blockWrongAnswers" nameLabel="Image_Identification_property_block_wrong_answers" type="boolean"/>
		<property isLocalized="true" name="Alt text" nameLabel="Image_Identification_property_alt_text" type="string"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Image_Identification_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Image_Identification_property_speech_texts" type="staticlist">

            <property name="Selected" nameLabel="Image_Identification_selected" type="staticrow">
                <property name="Selected" nameLabel="Image_Identification_selected" type="string"/>
            </property>

            <property name="Deselected" nameLabel="Image_Identification_deselected" type="staticrow">
                <property name="Deselected" nameLabel="Image_Identification_deselected" type="string"/>
            </property>

            <property name="Correct" nameLabel="Image_Identification_correct" type="staticrow">
                <property name="Correct" nameLabel="Image_Identification_correct" type="string"/>
            </property>

            <property name="Wrong" nameLabel="Image_Identification_wrong" type="staticrow">
                <property name="Wrong" nameLabel="Image_Identification_wrong" type="string"/>
            </property>

        </property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
	</model>
<css>.image-identification-element-selected,
.image-identification-element-correct,
.image-identification-element-incorrect,
.image-identification-element-mouse-hover,
.image-identification-element-show-answers,
.image-identification-element {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.image-identification-background-image {
    border: 0 solid white;
    padding: 0;
    margin: 0;
    background-size: 100%;
    background-position: 0 0;
}

.printable-image-identification-element {
    width: 100%;
}

.printable-image-identification-empty-div,
.printable-image-identification-empty-user-answer-div,
.printable-image-identification-empty-answer-div,
.printable-image-identification-selected-answer-div,
.printable-image-identification-selected-user-answer-div {
    width: 20px;
    height: 20px;
    position: relative;
    border: 3px solid #999;
    text-align: center;
}

.printable-image-identification-selected-answer-div::after {
    color: #999;
    content: "\2714";
}
.printable-image-identification-selected-user-answer-div::after {
    color: #000000;
    content: "\2714";
}

.printable-image-identification-empty-correct-answer-div,
.printable-image-identification-empty-incorrect-answer-div
{
    width: 20px;
    height: 20px;
    position: relative;
    border: 3px solid #999;
    text-align: center;
}
.printable-image-identification-selected-correct-answer-div
{
    width: 20px;
    height: 20px;
    position: relative;
    border: 3px solid #090;
    text-align: center;
}

.printable-image-identification-selected-correct-answer-div::after {
    color: #090;
    content: "\2714";
}

.printable-image-identification-selected-incorrect-answer-div
{
    width: 20px;
    height: 20px;
    position: relative;
    border: 3px solid #900;
    text-align: center;
}


.printable-image-identification-selected-incorrect-answer-div::after {
    color: #900;
    content: "\2718";
}

.printable-image-identification-empty-img,
.printable-image-identification-selected-answer-img,
.printable-image-identification-empty-answer-img,
.printable-image-identification-selected-user-answer-img,
.printable-image-identification-empty-user-answer-img,
.printable-image-identification-empty-correct-answer-img,
.printable-image-identification-empty-incorrect-answer-img,
.printable-image-identification-selected-correct-answer-img,
.printable-image-identification-selected-incorrect-answer-img {
    margin-left: 30px;
}
</css><view/><preview/><presenter>function AddonImage_Identification_create(){
    var presenter = function() {};

    var playerController;
    var eventBus;
    var isWCAGOn = false;

    presenter.isGradualShowAnswersActive = false;
    presenter.printableState = null;
    presenter.printableStateMode = 0;
    presenter.GSAcounter = 0;
    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

    presenter.lastEvent = null;
    presenter.isDisabled = false;

    var CSS_CLASSES = {
        ELEMENT : "image-identification-element",
        SELECTED : "image-identification-element-selected",
        CORRECT : "image-identification-element-correct",
        EMPTY : "image-identification-element-empty",
        INCORRECT : "image-identification-element-incorrect",
        MOUSE_HOVER : "image-identification-element-mouse-hover",
        SHOW_ANSWERS : "image-identification-element-show-answers",
        MOUSE_HOVER_SELECTED: "image-identification-element-selected-mouse-hover",
        IS_DISABLED: "image-identification-element-disabled"
    };

    /**
     * @return {string}
     */
    function CSS_CLASSESToString() {
        return CSS_CLASSES.ELEMENT + " " + CSS_CLASSES.SELECTED + " " + CSS_CLASSES.CORRECT + " " +
            CSS_CLASSES.EMPTY + " " + CSS_CLASSES.INCORRECT + " " + CSS_CLASSES.MOUSE_HOVER + " " +
            CSS_CLASSES.SHOW_ANSWERS + " " + CSS_CLASSES.MOUSE_HOVER_SELECTED + " " + CSS_CLASSES.IS_DISABLED;
    }

    function clickLogic() {
        if(presenter.isDisabled){
            return;
        }

        if (presenter.configuration.isErrorCheckMode &amp;&amp; (presenter.configuration.isActivity || presenter.configuration.isBlockedInErrorCheckingMode)) return;
        presenter.toggleSelectionState(true);
        applySelectionStyle(presenter.configuration.isSelected, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        var score = presenter.configuration.shouldBeSelected ? 1 : 0;
        if(score == 0 &amp;&amp; presenter.configuration.blockWrongAnswers) {
            presenter.toggleSelectionState(false);
            applySelectionStyle(presenter.configuration.isSelected, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
        }
    }

    presenter.handleMouseActions = function() {
        var $element = presenter.$view.find('div:first');

        $element.hover(
            function() {
                if (presenter.configuration.isErrorCheckMode &amp;&amp; (presenter.configuration.isActivity || presenter.configuration.isBlockedInErrorCheckingMode)) return;

                if (presenter.configuration.isHoverEnabled) {
                    $(this).removeClass(CSS_CLASSESToString());
                    $(this).addClass('image-identification-element-mouse-hover');
                    if(presenter.configuration.isSelected){
                        $(this).addClass('image-identification-element-selected-mouse-hover');
                    }
                    presenter.isDisabled ? presenter.disable() : presenter.enable();
                }
            },
            function() {
                if (presenter.configuration.isErrorCheckMode &amp;&amp; (presenter.configuration.isActivity || presenter.configuration.isBlockedInErrorCheckingMode)) return;

                if (presenter.configuration.isHoverEnabled) {
                    $(this).removeClass(CSS_CLASSESToString());
                    $(this).addClass(presenter.configuration.isSelected ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
                    presenter.isDisabled ? presenter.disable() : presenter.enable();
                }
            }
        );

        if (MobileUtils.isEventSupported('touchstart') || MobileUtils.isMobileUserAgent(window.navigator.userAgent)) {
            connectTouchHandlers($element);
        }

        connectClickHandlers($element);
    };

    function connectClickHandlers($element) {
        $element.on('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();

            presenter.lastEvent = e;
        });

        $element.on ('mouseup', function (e) {
            e.preventDefault();
            e.stopPropagation();

            if ( presenter.lastEvent.type != e.type ) {
                clickLogic();
            }
        });
    }

    function connectTouchHandlers($element) {
        $element.on('touchstart', function (e) {
            e.preventDefault();
            e.stopPropagation();

            presenter.lastEvent = e;
        });

        $element.on('touchend', function (e) {
            e.preventDefault();
            e.stopPropagation();

            if ( presenter.lastEvent.type != e.type ) {
                clickLogic();
            }
        });
    }

    function setViewDimensions(model) {
        var viewDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var viewDistances = DOMOperationsUtils.calculateOuterDistances(viewDimensions);

        presenter.$view.css({
            width:(model.Width - viewDistances.horizontal) + 'px',
            height:(model.Height - viewDistances.vertical) + 'px'
        });
    }

    function loadImage(imageSrc, isPreview) {
        var image = document.createElement('img');
        var $image = $(image);
        $image.attr('src', imageSrc);
        $image.addClass(presenter.configuration.isSelected ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
        presenter.$view.html(image);

        presenter.setVisibility(presenter.configuration.isVisibleByDefault || isPreview);

        if (presenter.configuration.altText !== undefined) {
            $image.attr("alt", presenter.configuration.altText);
        }

        $image.load(function () {
            var elementDimensions = DOMOperationsUtils.getOuterDimensions(this);
            var elementDistances = DOMOperationsUtils.calculateOuterDistances(elementDimensions);

            $(this).remove();

            var element = document.createElement('div');
            var innerElement = document.createElement('div');
            $(element).addClass(presenter.configuration.isSelected ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
            $(element).css({
                width:(presenter.$view.width() - elementDistances.horizontal) + 'px',
                height:(presenter.$view.height() - elementDistances.vertical) + 'px'
            });

            $(innerElement).addClass('image-identification-background-image');
            $(innerElement).css({
                backgroundImage:"url('" + imageSrc + "')",
                width:$(element).width() + 'px',
                height:$(element).height() + 'px',
                color: 'rgba(0,0,0,0.0)'
            });

            $(element).html(innerElement);
            presenter.$view.html(element);

            if (!isPreview) {
                presenter.handleMouseActions();
            }

            presenter.configuration.isDisabled ? presenter.disable() : presenter.enable();

            presenter.$view.trigger("onLoadImageCallbackEnd", []);
            presenter.configuration.isImageLoaded = true;

            $(element).on("click", function(e){
                e.preventDefault();
                e.stopPropagation();
            });
        });
    }

    function presenterLogic(view, model, preview) {
        presenter.$view = $(view);
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);

        setViewDimensions(model);

        if (ModelValidationUtils.isStringEmpty(presenter.configuration.imageSrc)) {
            return;
        }

        loadImage(presenter.configuration.imageSrc, preview);
        presenter.setTabindex(presenter.$view, presenter.configuration.isTabindexEnabled);

    }

    presenter.validateModel = function (model) {
        var newSpeechTexts = setSpeechTexts(model['speechTexts']);
        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]);

        return {
            addonID: model.ID,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isSelected: false,
            imageSrc: model.Image,
            shouldBeSelected: ModelValidationUtils.validateBoolean(model.SelectionCorrect),
            isHoverEnabled: true,
            isActivity: !ModelValidationUtils.validateBoolean(model["Is not an activity"]),
            isBlockedInErrorCheckingMode: ModelValidationUtils.validateBoolean(model["Block in error checking mode"]),
            isErrorCheckMode: false,
            blockWrongAnswers: ModelValidationUtils.validateBoolean(model.blockWrongAnswers),
            isTabindexEnabled: isTabindexEnabled,
            altText: model["Alt text"],
            isDisabled: ModelValidationUtils.validateBoolean(model["Is Disabled"]),
            langTag: model["langAttribute"],
            speechTexts: newSpeechTexts
        };
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeFrom_01(model);
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = '';
        }

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {
                Selected: {Selected: 'Selected'},
                Deselected: {Deselected: 'Deselected'},
                Correct: {Correct: 'Correct'},
                Wrong: {Wrong: 'Wrong'}
            };
        }

        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setSpeechTexts (speechTexts) {
        var newSpeechTexts = {
            selected:  'selected',
            deselected: 'deselected',
            correct: 'correct',
            wrong: 'wrong'
        };

        if (!speechTexts) {
            return newSpeechTexts;
        }

        newSpeechTexts = {
            selected:     getSpeechTextProperty(speechTexts['Selected']['Selected'], newSpeechTexts.selected),
            deselected:   getSpeechTextProperty(speechTexts['Deselected']['Deselected'], newSpeechTexts.deselected),
            correct:      getSpeechTextProperty(speechTexts['Correct']['Correct'], newSpeechTexts.correct),
            wrong:        getSpeechTextProperty(speechTexts['Wrong']['Wrong'], newSpeechTexts.wrong)
        };

        return newSpeechTexts;
    }

    function applySelectionStyle(selected, selectedClass, unselectedClass) {
        var element = presenter.$view.find('div:first')[0];

        $(element).removeClass(CSS_CLASSESToString());
        $(element).addClass(selected ? selectedClass : unselectedClass);
    }

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isActivity &amp;&amp; presenter.configuration.isErrorCheckMode) return;

        var commands = {
            'select': presenter.select,
            'deselect': presenter.deselect,
            'isAllOK': presenter.isAllOK,
            'show': presenter.show,
            'hide': presenter.hide,
            'isSelected': presenter.isSelected,
            'markAsCorrect': presenter.markAsCorrect,
            'markAsWrong': presenter.markAsWrong,
            'markAsEmpty': presenter.markAsEmpty,
            'removeMark': presenter.removeMark,
            'showAnswers': presenter.showAnswers,
            'hideAnswers': presenter.hideAnswers,
            'disable': presenter.disable,
            'enable': presenter.enable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.disable = function() {
        presenter.isDisabled = true;
        var $element = presenter.$view.find('div:first');
        $($element).addClass('image-identification-element-disabled');
    };

    presenter.enable = function() {
        presenter.isDisabled = false;
        var $element = presenter.$view.find('div:first');
        $($element).removeClass('image-identification-element-disabled');
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenter.eventBus = playerController.getEventBus();
        addonID = model.ID;

        presenterLogic(view, model, false);

        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            presenter.eventBus.addEventListener(events[i], this);
        }
    };

    presenter.reset = function() {
        presenter.configuration.isSelected = false;
        presenter.configuration.isErrorCheckMode = false;

        applySelectionStyle(false, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        if (presenter.configuration.isVisibleByDefault) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.configuration.isDisabled ? presenter.disable() : presenter.enable();
    };

    presenter.setWorkMode = function() {
        presenter.configuration.isErrorCheckMode = false;

        if (!presenter.configuration.isActivity) return;

        applySelectionStyle(presenter.configuration.isSelected, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        presenter.isDisabled ? presenter.disable() : presenter.enable()
    };

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isErrorCheckMode = true;

        if (!presenter.configuration.isActivity) return;

        if (presenter.configuration.isSelected) {
            applySelectionStyle(presenter.configuration.isSelected === presenter.configuration.shouldBeSelected, CSS_CLASSES.CORRECT, CSS_CLASSES.INCORRECT);
        } else {
            applySelectionStyle(true, CSS_CLASSES.EMPTY, CSS_CLASSES.ELEMENT);
        }

        presenter.isDisabled ? presenter.disable() : presenter.enable()
    };

    presenter.getErrorCount = function() {
        if (!presenter.configuration.isActivity) return 0;

        if (!presenter.configuration.shouldBeSelected) {
            return presenter.configuration.isSelected ? 1 : 0;
        }
        return 0;
    };

    presenter.getMaxScore = function() {
        if (!presenter.configuration.isActivity) return 0;

        if (presenter.configuration.shouldBeSelected) {
            return 1;
        } else {
            return 0;
        }
    };

    presenter.getScore = function() {
        if (!presenter.configuration.isActivity) return 0;

        if (presenter.configuration.shouldBeSelected) {
            return presenter.configuration.isSelected ? 1 : 0;
        }
        return 0;
    };

    presenter.getState = function() {
        return JSON.stringify({
            isSelected: presenter.configuration.isSelected,
            isVisible: presenter.configuration.isVisible,
            isDisabled: presenter.isDisabled
        });
    };

    function loadImageEndCallback() {
        applySelectionStyle(presenter.configuration.isSelected, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
        presenter.isDisabled ? presenter.disable() : presenter.enable();
        presenter.setVisibility(presenter.configuration.isVisible);
    }

    presenter.setState = function(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        var state = JSON.parse(stateString);
        presenter.configuration.isSelected = state.isSelected;
        presenter.configuration.isVisible = state.isVisible;

        presenter.$view.bind("onLoadImageCallbackEnd", function () {
            if(state.isDisabled !== undefined){
                presenter.isDisabled = state.isDisabled;
            }
            loadImageEndCallback();
        });

        if (presenter.configuration.isImageLoaded) {
            if(state.isDisabled !== undefined){
                presenter.isDisabled = state.isDisabled;
            }
            loadImageEndCallback();
        }
    };

    presenter.createEventData = function(isSelected, shouldBeSelected) {
        var score;
        if (presenter.configuration.isActivity){
            score = shouldBeSelected ? '1' : '0';
        } else {
            score = 0;
        }

        return {
            source : presenter.configuration.addonID,
            item : '',
            value : isSelected ? '1' : '0',
            score : score
        };
    };

    presenter.triggerSelectionEvent = function(isSelected, shouldBeSelected) {
        var eventData = this.createEventData(isSelected, shouldBeSelected);

        if (playerController != null) {
            playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    presenter.select = function (dontSendEvent) {
        presenter.configuration.isSelected = true;
        if (!dontSendEvent) {
            presenter.triggerSelectionEvent(true, presenter.configuration.shouldBeSelected);
        }
        applySelectionStyle(true, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
    };

    presenter.deselect = function (dontSendEvent) {
        presenter.configuration.isSelected = false;
        if (!dontSendEvent) {
            presenter.triggerSelectionEvent(false, presenter.configuration.shouldBeSelected);
        }
        applySelectionStyle(false, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
    };

    presenter.toggleSelectionState = function(shouldSendEvent) {
        presenter.configuration.isSelected = !presenter.configuration.isSelected;

        if(shouldSendEvent){
            presenter.triggerSelectionEvent(presenter.configuration.isSelected, presenter.configuration.shouldBeSelected);
        }

        if (isWCAGOn) {
            var speechVoices = [];
            if (presenter.configuration.isSelected) {
                speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.selected));
            } else {
                speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.deselected));
            }
            speak(speechVoices);
        }
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.isSelected = function () {
        return presenter.configuration.isSelected;
    };

    presenter.markAsCorrect = function() {
        presenter.configuration.isHoverEnabled = false;
        presenter.configuration.isMarked = true;

        // Selection can be changed only in activity mode.
        // When module is not in activity mode we want to be able to restore selection after removing mark classes
        if (presenter.configuration.isActivity) {
            presenter.configuration.isSelected = true;
        }

        applySelectionStyle(true, CSS_CLASSES.CORRECT, CSS_CLASSES.ELEMENT);
    };

    presenter.markAsWrong = function() {
        presenter.configuration.isHoverEnabled = false;
        presenter.configuration.isMarked = true;

        // Selection can be changed only in activity mode.
        // When module is not in activity mode we want to be able to restore selection after removing mark classes
        if (presenter.configuration.isActivity) {
            presenter.configuration.isSelected = true;
        }

        applySelectionStyle(true, CSS_CLASSES.INCORRECT, CSS_CLASSES.ELEMENT);
    };

    presenter.markAsEmpty = function() {
        presenter.configuration.isHoverEnabled = false;
        presenter.configuration.isMarked = true;

        applySelectionStyle(true, CSS_CLASSES.EMPTY, CSS_CLASSES.ELEMENT);
    };

    presenter.removeMark = function() {
        if (!presenter.configuration.isMarked) return;

        presenter.configuration.isHoverEnabled = true;
        presenter.configuration.isMarked = false;

        // Selection can be changed only in activity mode.
        // When module is not in activity mode we want to be able to restore selection after removing mark classes
        if (presenter.configuration.isActivity) {
            presenter.configuration.isSelected = true;
        }

        applySelectionStyle(presenter.configuration.isSelected, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
    };

    presenter.getActivitiesCount = function () {
        if(presenter.configuration.shouldBeSelected) {
            return 1;
        }
        return 0;
    }

    presenter.onEventReceived = function (eventName, data) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        } else if(eventName === "GradualShowAnswers") {
            presenter.GSAcounter++;
            if (!presenter.isGradualShowAnswersActive) {
                presenter.isGradualShowAnswersActive = true;
            } 
            if(presenter.GSAcounter === 1) presenter.hideStudentAnswersForGSA();
            if (data.moduleID === presenter.configuration.addonID) {
                presenter.showAnswers();
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.hideAnswers();
            presenter.isGradualShowAnswersActive = false;
        }
    };

    function applySelectionStyleShowAnswers (style){
        var element = presenter.$view.find('div:first')[0];
        $(element).addClass(style);
    }

    function applySelectionStyleHideAnswers (style){
        var element = presenter.$view.find('div:first')[0];

        $(element).removeClass(style);
    }

    presenter.hideStudentAnswersForGSA = function () {
        presenter.isShowAnswersActive = true;
        presenter.configuration.isErrorCheckMode = true;

        presenter.$view.find('.image-identification-element-selected').removeClass(CSS_CLASSES.SELECTED).addClass("image-identification-element was-selected");
    }

    presenter.showAnswers = function () {
        if(!presenter.configuration.isActivity){
            return;
        }

        presenter.isShowAnswersActive = true;

        presenter.configuration.isErrorCheckMode = true;

        presenter.$view.find('.image-identification-element-incorrect').removeClass(CSS_CLASSES.INCORRECT).addClass("image-identification-element was-selected");
        presenter.$view.find('.image-identification-element-correct').removeClass(CSS_CLASSES.CORRECT).addClass("image-identification-element was-selected");

        if(presenter.configuration.shouldBeSelected){
            applySelectionStyleShowAnswers(CSS_CLASSES.SHOW_ANSWERS);
        }else{
            presenter.$view.find('.image-identification-element-selected').removeClass(CSS_CLASSES.SELECTED).addClass("image-identification-element was-selected");
        }
    };

    presenter.hideAnswers = function () {
        if(!presenter.configuration.isActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.configuration.isErrorCheckMode = false;

        applySelectionStyleHideAnswers(CSS_CLASSES.SHOW_ANSWERS);

         var elementWasSelected = presenter.$view.find('.was-selected');
         $(elementWasSelected).addClass(CSS_CLASSES.SELECTED).removeClass("was-selected");

        presenter.isShowAnswersActive = false;
        presenter.GSAcounter = 0;
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        event.preventDefault();

        if (keycode === window.KeyboardControllerKeys.SPACE) {
            clickLogic();
        } else if (keycode === window.KeyboardControllerKeys.ENTER) {
            presenter.readAltText();
        }
    };

    presenter.setTabindex = function (element, isTabindexEnabled) {
        var tabindexValue = isTabindexEnabled ? "0" : "-1";
        element.attr("tabindex", tabindexValue);
    };

    presenter.readAltText = function() {
        var speechVoices = [];
        speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.altText, presenter.configuration.langTag));

        if( (presenter.configuration.isSelected &amp;&amp; !presenter.isShowAnswersActive) || (presenter.isShowAnswersActive &amp;&amp; presenter.configuration.shouldBeSelected)) {
            speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.selected));
        }

        if( presenter.$view.find('.' + CSS_CLASSES.CORRECT).size() &gt; 0) {
            speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.correct));
        } else if( presenter.$view.find('.' + CSS_CLASSES.INCORRECT).size() &gt; 0) {
            speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.wrong));
        }
        speak(speechVoices);
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.isEnterable = function() {return false};

    function isPrintableEmptyAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.EMPTY;
    }
    function isPrintableShowAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
    }
    function isPrintableShowUserAnswersStateMode () {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
    }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            presenter.printableStateMode = showAnswers ? presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS : presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        }
        else {
            presenter.printableStateMode = showAnswers ? presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS : presenter.PRINTABLE_STATE_MODE.EMPTY;
        }        
    }

    function setViewAndImgClasses(viewClass, imageClass, $img, $checkbox) {
        const prefix = 'printable-image-identification';
        const viewClassName = `${prefix}-${viewClass}`;
        const imageClassName = `${prefix}-${imageClass}`;
        $checkbox.addClass(viewClassName);
        $img.addClass(imageClassName);
    }

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        chosePrintableStateMode(showAnswers);
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        presenter.$view = $("&lt;div&gt;&lt;/div&gt;");
        presenter.$view.attr("id", presenter.configuration.addonID);
        presenter.$view.addClass("printable_addon_Image_Identification");

        loadImage(presenter.configuration.imageSrc, true);
        const $img = presenter.$view.find('.image-identification-element');
        $img.removeClass('image-identification-element');

        presenter.$view.prepend("&lt;div class='checkbox-identifier'&gt;&lt;/div&gt;")
        const $checkbox = presenter.$view.find('.checkbox-identifier')
        $checkbox.removeClass('checkbox-identifier');

        if (isPrintableEmptyAnswersStateMode()) {          
            setViewAndImgClasses('empty-div', 'empty-img', $img, $checkbox);                               // EMPTY STATE
        } else if (isPrintableShowAnswersStateMode()) {                                             // SHOW ANSWERS
            if (presenter.configuration.shouldBeSelected) {
                setViewAndImgClasses('selected-answer-div', 'selected-answer-img', $img, $checkbox);
            } else {
                setViewAndImgClasses('empty-answer-div', 'empty-answer-img', $img, $checkbox);
            }
        } else if (isPrintableShowUserAnswersStateMode()) {                                           // SHOW USER ANSWERS
            if (presenter.printableState.isSelected) {
                setViewAndImgClasses('selected-user-answer-div', 'selected-user-answer-img', $img, $checkbox);
            } else {
                setViewAndImgClasses('empty-user-answer-div', 'empty-user-answer-img', $img, $checkbox);
            }
        } else {                                                                                        // CHECK USER ANSWERS
            if (!presenter.printableState.isSelected &amp;&amp; !presenter.configuration.shouldBeSelected) {
                setViewAndImgClasses('empty-correct-answer-div', 'empty-correct-answer-img', $img, $checkbox);
            } else if (!presenter.printableState.isSelected &amp;&amp; presenter.configuration.shouldBeSelected) {
                setViewAndImgClasses('empty-incorrect-answer-div', 'empty-incorrect-answer-img', $img, $checkbox);
            } else if (presenter.printableState.isSelected &amp;&amp; presenter.configuration.shouldBeSelected) {
                setViewAndImgClasses('selected-correct-answer-div', 'selected-correct-answer-img', $img, $checkbox);
            } else {
                setViewAndImgClasses('selected-incorrect-answer-div', 'selected-incorrect-answer-img', $img, $checkbox);
            }
        }

        return presenter.$view[0].outerHTML;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Image_Viewer_Button_Controlled_Public" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Image" nameLabel="Image_Viewer_Button_Controlled_Public_property_image" type="image"/>
		<property name="Frames" nameLabel="Image_Viewer_Button_Controlled_Public_property_frames" type="string"/>
		<property name="Frame size" nameLabel="Image_Viewer_Button_Controlled_Public_property_frame_size" type="{Original, Keep aspect ratio, Stretch}"/>
		<property name="Sounds" nameLabel="Image_Viewer_Button_Controlled_Public_property_sounds" type="list">
			<property name="MP3 sound" nameLabel="Image_Viewer_Button_Controlled_Public_property_mp3_sound" type="audio"/>
			<property name="OGG sound" nameLabel="Image_Viewer_Button_Controlled_Public_property_ogg_sound" type="audio"/>
			<property name="AAC sound" nameLabel="Image_Viewer_Button_Controlled_Public_property_aac_sound" type="audio"/>
		</property>
		<property name="Frame names" nameLabel="Image_Viewer_Button_Controlled_Public_property_frame_names" type="list">
			<property name="name" nameLabel="Image_Viewer_Button_Controlled_Public_property_name" type="string"/>
			<property name="frame" nameLabel="Image_Viewer_Button_Controlled_Public_property_frames" type="string"/>
		</property>
		<property name="isClickDisabled" nameLabel="Image_Viewer_Button_Controlled_Public_property_is_click_disabled" type="boolean"/>
		<property name="Labels" nameLabel="Image_Viewer_Button_Controlled_Public_property_labels" type="list">
			<property isLocalized="true" name="Text" nameLabel="Image_Viewer_Button_Controlled_Public_property_text" type="string"/>
			<property name="Top" nameLabel="Image_Viewer_Button_Controlled_Public_property_top" type="string"/>
			<property name="Left" nameLabel="Image_Viewer_Button_Controlled_Public_property_left" type="string"/>
			<property name="Frames" nameLabel="Image_Viewer_Button_Controlled_Public_property_frames" type="string"/>
		</property>
		<property name="Show frame" nameLabel="Image_Viewer_Button_Controlled_Public_property_show_frame" type="string"/>
	</model>
<css>.image-viewer {
    border: none;
    padding: 0;
    margin: 0;
    float: left;
    overflow: hidden;
    background: no-repeat 0 0;
}

.image-viewer-audio-wrapper {
    display: none;
}

.image-viewer-audio-wrapper audio {
    display: inherit;
}

.image-viewer-hidden-image {
    display: none;
}

.image-viewer-loading-image {
    border: 0;
    margin: 0;
    padding: 0;
    display: none;
    position: absolute;
    z-index: 10;
    width: 50px;
    height: 50px;
}

.image-viewer-label {
    border: 0;
    margin: 0;
    padding: 0;
    position: absolute;
}
</css><view>&lt;div class='image-viewer'&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="image-viewer-loading-image"/&gt;
</view><preview>&lt;div class='image-viewer'&gt;
    &lt;!-- Here goes the image --&gt;
&lt;/div&gt;
</preview><presenter>function AddonImage_Viewer_Button_Controlled_Public_create(){
    var presenter = function(){};

    var viewerElement;
    var loadingScreen = {
        element: null,
        counter: 0
    };
    var audioElements = [];

    presenter.lastMovePosition = null;
    presenter.totalPercentage = 0;
    presenter.lastReceivedEvent = null;
    presenter.isMouseDown = false;

    presenter.ERROR_CODES = {
        'I_01': "Image must be uploaded to display Addon!",
        'FN_01': "Frame name cannot be empty!",
        'FN_02': "Frame number must be set to properly display Addon",
        'FN_03': "Frame number is not a number!",
        'FN_04': "Frame number must be a positive integer!",
        'FN_05': "Frame names - frame number must be between 1 and frames count",
        'L_01': "Label text cannot be empty!",
        'L_02': "Label top value incorrect!",
        'L_03': "Label left value incorrect!",
        'L_04': "All label fields must be filled correctly!",
        'FL_01': "Label must be assigned to at least one frame!",
        'FL_02': "Frames list syntax incorrect! Check for separators!",
        'FL_03': "Frame number is not a number!",
        'FL_04': "Frame number must be positive integer!",
        'FL_05': "Frame number cannot be higher than Frames!",
        'FL_06': "Frame number missing inside list!",
        'FL_07': "Frame numbers range incorrect!"
    };

    presenter.FRAME_SIZE = {
        ORIGINAL: 0,
        SCALED: 1,
        STRETCHED: 2
    };

    function setContainerDimensions(width, height) {
        var viewerDimensions = DOMOperationsUtils.getOuterDimensions(viewerElement);
        var viewerDistances = DOMOperationsUtils.calculateOuterDistances(viewerDimensions);
        var viewerWidth = width - viewerDistances.horizontal;
        var viewerHeight = height - viewerDistances.vertical;

        $(viewerElement).css({
            width: viewerWidth,
            height: viewerHeight
        });

        presenter.configuration.containerDimensions = {
            width: viewerWidth,
            height: viewerHeight
        };
    }

    function loadSounds(){
        // Check for browser audio tag support
        if (!buzz.isSupported()) {
            return;
        }

        buzz.defaults.autoplay = false;
        buzz.defaults.loop = false;
        showLoadingScreen();

        for (var i = 0; i &lt; presenter.configuration.frames; i++) {
            if ((i &gt; presenter.configuration.sounds.length - 1 ) || presenter.configuration.sounds[i].isEmpty) {
                audioElements[i] = null;
            } else {
                if (presenter.configuration.sounds[i].MP3 !== "" &amp;&amp; buzz.isMP3Supported()) {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].MP3);
                } else if (presenter.configuration.sounds[i].OGG !== "" &amp;&amp; buzz.isOGGSupported()) {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].OGG);
                } else {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].AAC);
                }

                audioElements[i].load();
            }
        }

        hideLoadingScreen();
    }


    function getDirection(type, touch) {
        var direction = null;

        if(type == 'touchstart') {
            presenter.lastMovePosition = touch.pageX;
            return false;
        } else if(presenter.lastMovePosition == touch.pageX) {
            return false;
        } else {
            direction = presenter.lastMovePosition &gt; touch.pageX ? 'left' : 'right';
            presenter.lastMovePosition = touch.pageX;
        }

        if(type == 'touchend') {
            presenter.lastMovePosition = null;
            return false;
        }

        return direction;
    }

    function getMouseDirection(evt) {
        var direction = null;
        if(presenter.lastMovePosition &gt; evt.pageX) {
            direction = 'left';
        } else if(presenter.lastMovePosition &lt; evt.pageX){
            direction = 'right';
        } else {
            direction = null;
        }

        presenter.lastMovePosition = evt.pageX;
        return direction;
    }

    function setTotalPercentage(percentage, direction) {
        if(direction == 'right') {
            presenter.totalPercentage += percentage;
        } else {
            presenter.totalPercentage -= percentage;
        }
    }

    function calculateShift() {
        return (presenter.configuration.frameWidthPercentage / 100) * 30;
    }

    function shouldChangeFrame(shiftValue) {
        return (Math.abs(presenter.totalPercentage) &gt;= shiftValue) &amp;&amp;
            (presenter.lastReceivedEvent == 'touchend' || presenter.lastReceivedEvent == 'mouseup')
    }

    function shouldMoveBackToDefaultPosition() {
        return (presenter.lastReceivedEvent == 'touchend' || presenter.lastReceivedEvent == 'mouseup');
    }

    function changeFrameDependingOnShiftValue(shiftValue) {
        if(presenter.totalPercentage &lt;= shiftValue) {
            presenter.next();
        } else {
            presenter.previous();
        }
        presenter.totalPercentage = 0;
    }

    function moveBackToDefaultPosition() {
        $(viewerElement).css({
            'background-position' : presenter.configuration.currentFrame * presenter.configuration.frameWidthPercentage + '%'
        });
        presenter.totalPercentage = 0;
    }

    function calculateBackgroundPosition() {
        var base = (presenter.configuration.currentFrame * presenter.configuration.frameWidthPercentage);
        return base - presenter.totalPercentage;
    }

    function isTap(evt) {
        return presenter.lastReceivedEvent == 'touchstart' &amp;&amp; evt.type == 'touchend';
    }

    function isClick(evt) {
        return presenter.lastReceivedEvent == 'mousedown' &amp;&amp; evt.type == 'mouseup';
    }

    function setMouseDown(evt) {
        if(evt.type == 'mousedown') {
            presenter.isMouseDown = true;
            presenter.lastMovePosition = evt.pageX;
            viewerElement.unbind('click');
        } else if(evt.type == 'mouseup') {
            presenter.isMouseDown = false;
            presenter.lastMovePosition = null;
        }
    }

    function handleClickAction() {
        viewerElement.click(function(e) {
            e.stopPropagation();

            presenter.configuration.currentFrame = presenter.configuration.currentFrame === presenter.configuration.frames - 1 ? 0 : presenter.configuration.currentFrame + 1;

            presenter.changeFrame(this, presenter.configuration, true);
        });
    }


    function handleMouseDragActions(shiftValue, percentage) {
        viewerElement.on('mousedown mousemove mouseup', function(evt) {
            evt.preventDefault();
            setMouseDown(evt);

            if(isClick(evt)) {
                handleClickAction();
                presenter.lastReceivedEvent = null;
            } else {
                presenter.lastReceivedEvent = evt.type;
            }

            if(evt.type != 'mousemove') {
                if(shouldChangeFrame(shiftValue)) {
                    changeFrameDependingOnShiftValue(shiftValue)
                } else if(shouldMoveBackToDefaultPosition()) {
                    moveBackToDefaultPosition();
                }
            }

            if(presenter.isMouseDown) {
                var direction = getMouseDirection(evt);
                if(direction) {
                    setTotalPercentage(percentage, direction);
                    $(this).css({
                        'background-position' : calculateBackgroundPosition() + '%'
                    })
                }
            }
        });
    }

    function handleTouchAction(shiftValue, percentage) {

        viewerElement.on('touchstart touchmove touchend', function(evt){
            evt.preventDefault();
            evt.stopPropagation();

            if(isTap(evt)) {
                presenter.lastReceivedEvent = null;
                viewerElement.trigger('click');
            } else {
                presenter.lastReceivedEvent = evt.type;
            }

            var touch = event.touches[0] || event.changedTouches[0];
            var direction = getDirection(evt.type, touch);

            if(shouldChangeFrame(shiftValue)) {
                changeFrameDependingOnShiftValue(shiftValue)
            } else if(shouldMoveBackToDefaultPosition()) {
                moveBackToDefaultPosition();
            }

            if(direction){
                setTotalPercentage(percentage, direction);
                $(this).css({
                    'background-position' : calculateBackgroundPosition() + '%'
                });
            }
        });
    }

    // Calculate scale for image containing element depending on frame aspect ratio
    presenter.calculateContainerDimensions = function(imageWidth, imageHeight, containerWidth, containerHeight) {
        var imageRatio = imageWidth / imageHeight;
        var containerRatio = containerWidth / containerHeight;

        var horizontal;
        var vertical;

        if (imageRatio &gt;= containerRatio) {
            horizontal = containerWidth;
            vertical = containerWidth / imageRatio;
        } else {
            vertical = containerHeight;
            horizontal = containerHeight * imageRatio;
        }


        return {
            horizontal: horizontal,
            vertical: vertical
        };
    };

    // This function returns string containing CSS declaration of elements
    // background image size in percentage measure
    function calculateBackgroundSize(size, framesCount) {
        var cssValue;

        switch(size) {
            case presenter.FRAME_SIZE.SCALED:
            case presenter.FRAME_SIZE.STRETCHED:
                cssValue = framesCount * 100;
                cssValue += '% 100%';
                break;
            default:
                cssValue = '';
        }

        return cssValue;
    }

    function loadImage(preview) {
        var tempImageElement = document.createElement('img');
        $(tempImageElement).addClass('image-viewer-hidden-image');
        $(tempImageElement).attr('src', presenter.configuration.imageSrc);
        $(presenter.$view).append(tempImageElement);

        $(tempImageElement).load(function() {
            var containerWidth = presenter.configuration.containerDimensions.width;
            var containerHeight = presenter.configuration.containerDimensions.height;
            var containerDimensions = presenter.calculateContainerDimensions($(this).width() / presenter.configuration.frames, $(this).height(), containerWidth, containerHeight);
            var backgroundSize = calculateBackgroundSize(presenter.configuration.frameSize, presenter.configuration.frames);

            var elementWidth;
            var elementHeight;

            switch (presenter.configuration.frameSize) {
                case presenter.FRAME_SIZE.ORIGINAL:
                    elementWidth = $(tempImageElement).width() / presenter.configuration.frames;
                    elementHeight = $(tempImageElement).height();
                    break;
                case presenter.FRAME_SIZE.SCALED:
                    elementWidth = containerDimensions.horizontal;
                    elementHeight = containerDimensions.vertical;
                    break;
                case presenter.FRAME_SIZE.STRETCHED:
                    elementWidth = containerWidth;
                    elementHeight = containerHeight;
                    break;
            }

            viewerElement.css({
                width: elementWidth + 'px',
                height: elementHeight + 'px',
                backgroundImage: "url('" + presenter.configuration.imageSrc + "')"
            });

            if (backgroundSize) {
                $(viewerElement).css('background-size', backgroundSize);
            }

            $(this).remove();
            presenter.changeFrame(viewerElement, presenter.configuration, false);

            hideLoadingScreen();
            presenter.$view.trigger("onLoadImageEnd", [preview]);
        });
    }

    function prepareLoadingScreen(containerWidth, containerHeight) {
        $(loadingScreen.element).css({
            top: ((containerHeight - $(loadingScreen.element).height()) / 2) + 'px',
            left: ((containerWidth - $(loadingScreen.element).width()) / 2) + 'px'
        });
    }

    function showLoadingScreen() {
        $(loadingScreen.element).show();
        loadingScreen.counter++;
    }

    function hideLoadingScreen() {
        loadingScreen.counter--;
        if (loadingScreen.counter === 0) {
            $(loadingScreen.element).hide();
        }
    }

    /**
     * Creates label SPAN element and appends it to Addon space
     *
     * @param label
     * @return newly created element DOM reference
     */
    function createLabelElement(label) {
        var labelElement = document.createElement('span');

        $(labelElement).addClass('image-viewer-label');
        $(labelElement).html(label.text);
        $(labelElement).css({
            top: label.top + 'px',
            left: label.left + 'px',
            visibility: 'hidden'
        });

        $(presenter.$view).append(labelElement);

        return labelElement;
    }

    function loadLabels() {
        for (var i = 0; i &lt; presenter.configuration.labels.length; i++) {
            presenter.configuration.labels[i].element = createLabelElement(presenter.configuration.labels[i]);
        }
    }

    function hideLabels() {
        $(presenter.$view).find('.image-viewer-label').css('visibility', 'hidden');
    }

    /**
     * Displays labels that are assigned to given frame.
     *
     * @param frame frame number counted from 1 to n
     */
    function displayLabels(frame) {
        hideLabels();

        for (var i = 0; i &lt; presenter.configuration.labels.length; i++) {
            var label = presenter.configuration.labels[i];
            for (var j = 0; j &lt; label.frames.length; j++) {
                if (frame === label.frames[j]) {
                    $(label.element).css('visibility', 'visible');
                }
            }
        }
    }

    function loadImagesCallback(isPreview) {
        if (!isPreview) {
            loadSounds();

            if (!presenter.configuration.isClickDisabled) {
                var shiftValue = calculateShift();
                var percentage = (viewerElement.width() / presenter.configuration.frames) / 100;
                handleClickAction();
                handleTouchAction(shiftValue, percentage);
                handleMouseDragActions(shiftValue, percentage);
            }

            presenter.setVisibility(presenter.configuration.defaultVisibility);
        }

        presenter.imageLoadedDeferred.resolve();
    }

    function loadImageEndCallback() {
        var configuration = presenter.configuration;

        presenter.setVisibility(presenter.configuration.currentVisibility);
        presenter.changeFrame(viewerElement, configuration, true);
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    function presenterLogic(view, model, preview) {
        presenter.imageLoadedDeferred = new jQuery.Deferred();
        presenter.imageLoaded = presenter.imageLoadedDeferred.promise();

        presenter.$view = $(view);
        viewerElement = presenter.$view.find('.image-viewer:first');
        loadingScreen.element = presenter.$view.find('.image-viewer-loading-image:first')[0];

        if (!preview) {
            var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
            if (loadingSrc) $(loadingScreen.element).attr('src', loadingSrc);
        }

        presenter.configuration = presenter.validateModel(model);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
        } else {
            showLoadingScreen();
            presenter.configuration.frameWidthPercentage = 100 / (presenter.configuration.frames - 1);
            presenter.configuration.flags = [];

            if (preview) {
                presenter.configuration.currentFrame = presenter.configuration.showFrame &lt;= presenter.configuration.frames ? presenter.configuration.showFrame - 1 : 0;
            } else {
                presenter.configuration.currentFrame = 0;
            }

            setContainerDimensions(model.Width, model.Height);
            prepareLoadingScreen(model.Width, model.Height);
            loadLabels();

            presenter.$view.bind("onLoadImageEnd", function (event, isPreview) {
                loadImagesCallback(isPreview);
            });

            loadImage(preview);
            if (presenter.configuration.currentVisibility) {
                displayLabels(0);
            }
        }
    }

    presenter.changeFlag = function (flag) {
        var frame, element = presenter.$view.find('.image-viewer:first')[0];
        flag = parseInt(flag, 10);

        if (!isNaN(flag) &amp;&amp; flag &gt; 0) {
            var oldFlag = presenter.configuration.flags[flag];
            presenter.configuration.flags[flag] = (!oldFlag || oldFlag == 0) ? 1 : 0;

            frame = 0;
            for (var i in presenter.configuration.flags) {
                if (presenter.configuration.flags[i] == 1) {
                    frame += Math.pow(2, i - 1);
                }
            }

            if (frame &gt;= 0 &amp;&amp; frame &lt;= presenter.configuration.frames - 1) {
                presenter.configuration.currentFrame = frame;
                presenter.changeFrame(element, presenter.configuration, true);
            }
        }
    };

    presenter.next = function() {
        var currentFrame = presenter.configuration.currentFrame;
        var framesCount = presenter.configuration.frames;

        presenter.configuration.currentFrame = currentFrame === framesCount - 1 ? 0 : currentFrame + 1;
        presenter.changeFrame(viewerElement, presenter.configuration, true);
    };

    presenter.previous = function() {
        var currentFrame = presenter.configuration.currentFrame;
        var framesCount = presenter.configuration.frames;

        presenter.configuration.currentFrame = currentFrame === 0 ? framesCount - 1 : currentFrame - 1;
        presenter.changeFrame(viewerElement, presenter.configuration, true);
    };

    presenter.changeFlagCommand = function (params) {
        presenter.changeFlag(params[0]);
    };

    presenter.moveToFrame = function (frame) {
        var element = presenter.$view.find('.image-viewer:first')[0];
        frame = parseInt(frame, 10);

        if (!isNaN(frame) &amp;&amp; frame &gt; 0 &amp;&amp; frame &lt;= presenter.configuration.frames) {
            presenter.configuration.currentFrame = frame - 1;
            presenter.changeFrame(element, presenter.configuration, true);
        }
    };

    presenter.moveToFrameCommand = function (params) {
        presenter.moveToFrame(params[0]);
    };

    presenter.moveToFrameName = function (frameName) {
        if (ModelValidationUtils.isStringEmpty(frameName) || presenter.configuration.frameNamesEmpty) {
            return;
        }

        var element = presenter.$view.find('.image-viewer:first')[0], frame, isFrameFound;

        isFrameFound = false;
        for (var i = 0; i &lt; presenter.configuration.frameNames.length; i++) {
            if (presenter.configuration.frameNames[i].name == frameName) {
                frame = presenter.configuration.frameNames[i].frame;
                isFrameFound = true;
                break;
            }
        }

        if (isFrameFound) {
            presenter.configuration.currentFrame = frame - 1;
            presenter.changeFrame(element, presenter.configuration, true);
        }
    };

    presenter.moveToFrameNameCommand = function (params) {
        presenter.moveToFrameName(params[0]);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'changeFlag': presenter.changeFlagCommand,
            'moveToFrame': presenter.moveToFrameCommand,
            'moveToFrameName': presenter.moveToFrameNameCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.currentVisibility = true;
        displayLabels(presenter.configuration.currentFrame + 1);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.currentVisibility = false;
        hideLabels();
    };

    presenter.createPreview = function(view, model){
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.validateFrameSize = function(frameSize) {
        var result;

        switch (frameSize) {
            case "Keep aspect ratio":
                result = presenter.FRAME_SIZE.SCALED;
                break;
            case "Stretch":
                result = presenter.FRAME_SIZE.STRETCHED;
                break;
            default:
                result = presenter.FRAME_SIZE.ORIGINAL;
                break;
        }

        return result;
    };

    /**
     * Function returns errorCode if any of this errors occurs:
     * FN_02 - frame number empty
     * FN_03 - frame number is not a number
     * FN_04 - frame number negative
     *
     * @return errorCode
     */
    presenter.validateFramesCount = function(frames) {
        if (!frames) {
            return {
                isError: true,
                errorCode: "FN_02"
            };
        } else {
            var parsedFrames = parseInt(frames, 10);
            if (isNaN(parsedFrames)) {
                return {
                    isError: true,
                    errorCode: "FN_03"
                };
            } else if (parsedFrames &lt; 1 ) {
                return {
                    isError: true,
                    errorCode: "FN_04"
                };
            }
        }

        return {
            isError: false,
            frames : parsedFrames
        };
    };

    presenter.validateSound = function(soundsArray) {
        var sounds = [];

        if (soundsArray &amp;&amp; $.isArray(soundsArray)) {
            for (var i = 0; i &lt; soundsArray.length; i++) {
                var isMP3Empty = soundsArray[i]['MP3 sound'] === "" || soundsArray[i]['MP3 sound'] === "/file/";
                var isAACEmpty = soundsArray[i]['AAC sound'] === "" || soundsArray[i]['AAC sound'] === "/file/";
                var isOGGEmpty = soundsArray[i]['OGG sound'] === "" || soundsArray[i]['OGG sound'] === "/file/";
                var isEmpty = isMP3Empty &amp;&amp; isAACEmpty &amp;&amp; isOGGEmpty;

                sounds.push({
                    AAC : isAACEmpty ? "" : soundsArray[i]['AAC sound'],
                    OGG : isOGGEmpty ? "" : soundsArray[i]['OGG sound'],
                    MP3 : isMP3Empty ? "" : soundsArray[i]['MP3 sound'],
                    isEmpty : isEmpty
                });
            }
        }

        return {
            sounds : sounds
        };
    };

    /**
     * Function returns errorCode if any of this errors occurs:
     * FN_01 - frame name empty
     * FN_02 - frame number empty
     * FN_03 - frame number is not a number
     * FN_04 - frame number negative
     * FN_05 - frame number out of bounds
     *
     * @return errorCode
     */
    presenter.validateFrameNames = function(frameNames, frames) {
        var frameNamesArray = [];

        var frameNamesEmpty = false;
        if (frameNames &amp;&amp; $.isArray(frameNames)) {
            var firstName = frameNames[0].name;
            var firstFrame = frameNames[0].frame;
            if (firstFrame === "" &amp;&amp; firstName === "") {
                frameNamesEmpty = true;
            } else {
                for (var j = 0; j &lt; frameNames.length; j++) {
                    var frame = frameNames[j]['frame'];
                    var name = frameNames[j]['name'];

                    if (!name || name === null || name === "") return { isError: true, errorCode: "FN_01" };
                    if (!frame || frame === "") return { isError: true, errorCode: "FN_02" };

                    frame = parseInt(frame, 10);
                    if (isNaN(frame)) return { isError: true, errorCode: "FN_03" };
                    if (frame &lt;= 0) return { isError: true, errorCode: "FN_04" };
                    if (frame &gt; frames) return { isError: true, errorCode: "FN_05" };

                    frameNamesArray.push({ name: name, frame: frame });
                }
            }
        }

        return {
            isError: false,
            frameNames: frameNamesArray,
            frameNamesEmpty: frameNamesEmpty
        };
    };

    /**
     * This function validates and converts number from string representation to integer value
     */
    presenter.sanitizePosition = function (position) {
        if (!position) return { isError:true };

        var parsedPosition = parseInt(position, 10);
        if (isNaN(parsedPosition)) return { isError:true };
        if (parsedPosition &lt; 0) return { isError:true };

        return { isError:false, position:parseInt(position, 10) };
    };

    /**
     * Removes duplicates from sorted array of numbers
     *
     * @param array sorted array of numbers
     * @return array with removed duplicates
     */
    presenter.removeDuplicatesFromArray = function(array) {
        if (array.length === 0) {
            return [];
        }

        var results = [];

        for (var i = 0; i &lt; array.length - 1; i++) {
            if (array[i + 1] !== array[i]) {
                results.push(array[i]);
            }
        }

        results.push(array[array.length - 1]);

        return results;
    };

    /**
     * Validates frames numbers list separated with commas
     *
     * @param frames string representation of frames list
     * @param count frames count
     *
     * @return array of frames numbers counted from 1 to n
     * @returns FL_01 if list is empty or undefined
     * @returns FL_02 if list is incorrectly constructed
     * @returns FL_03 if frame number is not a number
     * @returns FL_04 if frame number is not positive integer
     * @returns FL_05 if frame number higher than frames count
     * @returns FL_06 if frame number is missing (inside list)
     * @returns FL_07 if frame numbers range is incorrect
     */
    presenter.validateFramesList = function (frames, count) {
        var list = [];

        if (!frames || frames.length === 0) {
            return {
                isError: true,
                errorCode: "FL_01"
            };
        }

        var regExp = new RegExp('[0-9a-zA-Z\,\-]+'); // Only digits and commas are allowed in slides list
        var matchResult = frames.match(regExp);
        if (matchResult === null || frames.length !== matchResult[0].length) {
            return {
                isError: true,
                errorCode: "FL_02"
            };
        }

        var splittedFrames = frames.split(',');
        for (var i = 0; i &lt; splittedFrames.length; i++) {
            if (splittedFrames[i] === "") {
                return {
                    isError: true,
                    errorCode: "FL_06"
                };
            }

            var indexOfRange = splittedFrames[i].search('-');
            if (indexOfRange !== -1) {
                var rangeStart = parseInt(splittedFrames[i].split('-')[0], 10);
                var rangeEnd = parseInt(splittedFrames[i].split('-')[1], 10);


                var isRangeError = isNaN(rangeStart) || isNaN(rangeEnd);
                isRangeError = isRangeError || rangeStart &lt; 1;
                isRangeError = isRangeError || rangeEnd &lt; 1;
                isRangeError = isRangeError || rangeStart &gt; rangeEnd;
                isRangeError = isRangeError || rangeStart &gt; count;
                isRangeError = isRangeError || rangeEnd &gt; count;
                if (isRangeError) {
                    return {
                        isError: true,
                        errorCode: "FL_07"
                    };
                }

                for (var frameNumber = rangeStart; frameNumber &lt;= rangeEnd; frameNumber++) {
                    list.push(frameNumber);
                }

                continue;
            }

            var frame = parseInt(splittedFrames[i]);

            if (isNaN(frame)) {
                return {
                    isError: true,
                    errorCode: "FL_03"
                };
            }

            if (frame &lt; 1) {
                return {
                    isError: true,
                    errorCode: "FL_04"
                };
            }

            if (frame &gt; count) {
                return {
                    isError: true,
                    errorCode: "FL_05"
                };
            }

            list.push(frame);
        }

        list = list.sort();
        list = presenter.removeDuplicatesFromArray(list);

        return {
            isError: false,
            list: list
        };
    };

    /** If validation error occurs then one of the following error codes are returned
     * @returns L_01 if text is empty
     * @returns L_02 if top value is invalid
     * @returns L_03 if left value is invalid
     * @returns L_04 if empty label and labels count &gt; 0
     * @returns FL_** if there was problem with frames property
     */
    presenter.validateLabels = function (labelsArray, framesCount) {
        var labels = [];

        // Ugly fix for Editor problems
        if (!labelsArray) return { isError:false, labels:labels };

        for (var i = 0; i &lt; labelsArray.length; i++) {
            var frames = labelsArray[i].Frames;
            var topPosition = labelsArray[i].Top;
            var leftPosition = labelsArray[i].Left;

            if (labelsArray[i].Text == "" &amp;&amp; topPosition == "" &amp;&amp; leftPosition == "" &amp;&amp; frames == "") {
                if (labelsArray.length == 1) {
                    labels = [];
                    break;
                } else return { isError:true, errorCode:"L_04" };
            }

            if (!labelsArray[i].Text || labelsArray[i].Text === "") return { isError:true, errorCode:"L_01" };

            var validatedFramesList = presenter.validateFramesList(frames, framesCount);
            if (validatedFramesList.isError) return { isError:true, errorCode: validatedFramesList.errorCode };

            var sanitizedTopPosition = presenter.sanitizePosition(topPosition);
            if (sanitizedTopPosition.isError) return { isError:true, errorCode:"L_02" };

            var sanitizedLeftPosition = presenter.sanitizePosition(leftPosition);
            if (sanitizedLeftPosition.isError)  return { isError:true, errorCode:"L_03" };

            var text = {
                text:labelsArray[i].Text,
                frames:validatedFramesList.list,
                top:sanitizedTopPosition.position,
                left:sanitizedLeftPosition.position
            };

            labels.push(text);
        }

        return { isError:false, labels:labels };
    };

    /**
     * Validates string representation of integer. Only positive integer values are allowed. If both (value and default) are
     * undefined then isError property is set to true.
     */
    presenter.validatePositiveInteger = function (value, defaultValue) {
        var isValueDefined = value != undefined &amp;&amp; value !== "";
        var isDefaultDefined = defaultValue != undefined &amp;&amp; !isNaN(defaultValue);

        if (!isValueDefined &amp;&amp; !isDefaultDefined) return { isError: true };

        if (!isValueDefined &amp;&amp; isDefaultDefined) return { isError: false, value: defaultValue };

        var parsedSize = parseInt(value, 10);
        if (isNaN(parsedSize) || parsedSize &lt; 1) return { isError: true };

        return { isError: false, value: parsedSize };
    };

    presenter.validateModel = function(model) {
        if (ModelValidationUtils.isStringEmpty(model.Image)) return { isError: true, errorCode: "I_01" };

        var validatedFrames = presenter.validateFramesCount(model.Frames);
        if (validatedFrames.isError) return { isError: true, errorCode: validatedFrames.errorCode };

        var validatedFrameNames = presenter.validateFrameNames(model["Frame names"], validatedFrames.frames);
        if (validatedFrameNames.isError) return { isError: true, errorCode: validatedFrameNames.errorCode };

        var validatedLabels = presenter.validateLabels(model.Labels, validatedFrames.frames);
        if (validatedLabels.isError) return { isError: true, errorCode: validatedLabels.errorCode };

        var showFrame = 1;
        var validatedShowFrame = presenter.validatePositiveInteger(model["Show frame"], 1);
        if (!validatedShowFrame.isError) {
            showFrame = validatedShowFrame.value;
        }

        var defaultVisibility = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        return {
            isError: false,
            imageSrc : model.Image,
            frames : validatedFrames.frames,
            sounds : presenter.validateSound(model.Sounds).sounds,
            frameNames: validatedFrameNames.frameNames,
            frameNamesEmpty: validatedFrameNames.frameNamesEmpty,
            isClickDisabled: ModelValidationUtils.validateBoolean(model.isClickDisabled),
            frameSize: presenter.validateFrameSize(model["Frame size"]),
            labels: validatedLabels.labels,
            showFrame: showFrame,
            defaultVisibility: defaultVisibility,
            currentVisibility: defaultVisibility
        };
    };

    // Frames are counted from 0 to (frameCount - 1)
    presenter.changeFrame = function(element, configuration, playAudio) {
        $(element).css('backgroundPosition', configuration.currentFrame * configuration.frameWidthPercentage + '% ' + ' 0%');
        if (presenter.configuration.currentVisibility) {
            displayLabels(configuration.currentFrame + 1);
        }

        if (configuration.currentFrame !== 0 &amp;&amp; playAudio) {
            presenter.playAudio(presenter.$view, configuration);
        } else if (configuration.currentFrame === 0) {
            stopAllAudio();
        }
    };

    function stopAllAudio() {
        for (var i = 0; i &lt; audioElements.length; i++) {
            if (audioElements[i] !== null) {
                stopAudio(audioElements[i]);
            }
        }
    }

    function stopAudio(audio) {
        audio.pause();
        audio.setTime(0);
    }

    presenter.playAudio = function(viewContainer, configuration) {
        stopAllAudio();

        var audio = audioElements[configuration.currentFrame - 1];
        if (audio) {
            audio.play();
        }
    };

    presenter.getState = function() {
        return JSON.stringify({
            "currentFrame" : this.configuration.currentFrame,
            "currentVisibility" : this.configuration.currentVisibility,
            "flags": presenter.configuration.flags
        });
    };

    presenter.setState = function(state) {
        var savedState = JSON.parse(state);

        var configuration = presenter.configuration;
        configuration.currentFrame = savedState["currentFrame"];
        configuration.currentVisibility = savedState["currentVisibility"];
        configuration.flags = savedState["flags"];

        $.when(presenter.imageLoaded).then(loadImageEndCallback);
    };

    presenter.reset = function () {
        stopAllAudio();
        presenter.configuration.flags = [];
        presenter.configuration.currentFrame = 0;
        presenter.changeFrame(viewerElement, presenter.configuration, true);
        displayLabels(1);
        presenter.setVisibility(presenter.configuration.defaultVisibility);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Image_Viewer_Public" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Image" nameLabel="Image_Viewer_Public_property_image" type="image"/>
		<property name="Frames" nameLabel="Image_Viewer_Public_property_frames" type="string"/>
		<property name="Frame size" nameLabel="Image_Viewer_Public_property_frame_size" type="{Original, Keep aspect ratio, Stretch}"/>
		<property name="Sounds" nameLabel="Image_Viewer_Public_property_sounds" type="list">
			<property name="MP3 sound" nameLabel="Image_Viewer_Public_property_mp3_sound" type="audio"/>
			<property name="OGG sound" nameLabel="Image_Viewer_Public_property_ogg_sound" type="audio"/>
			<property name="AAC sound" nameLabel="Image_Viewer_Public_property_aac_sound" type="audio"/>
		</property>
		<property name="Frame names" nameLabel="Image_Viewer_Public_property_frame_names" type="list">
			<property name="name" nameLabel="Image_Viewer_Public_property_name" type="string"/>
			<property name="frame" nameLabel="Image_Viewer_Public_property_frames" type="string"/>
		</property>
		<property name="Alternative texts" nameLabel="Image_Viewer_Public_property_alternative_texts" type="list">
			<property name="Alternative text" nameLabel="Image_Viewer_Public_property_alternative_text" type="text"/>
			<property name="frame" nameLabel="Image_Viewer_Public_property_frames" type="string"/>
		</property>
		<property name="isClickDisabled" nameLabel="Image_Viewer_Public_property_is_click_disabled" type="boolean"/>
		<property name="Labels" nameLabel="Image_Viewer_Public_property_labels" type="list">
			<property isLocalized="true" name="Text" nameLabel="Image_Viewer_Public_property_text" type="string"/>
			<property name="Top" nameLabel="Image_Viewer_Public_property_top" type="string"/>
			<property name="Left" nameLabel="Image_Viewer_Public_property_left" type="string"/>
			<property name="Frames" nameLabel="Image_Viewer_Public_property_frames" type="string"/>
		</property>
		<property name="Show watermark" nameLabel="Image_Viewer_Public_property_show_watermark" type="boolean"/>
		<property name="Watermark color" nameLabel="Image_Viewer_Public_property_watermark_color" type="string"/>
		<property name="Watermark opacity" nameLabel="Image_Viewer_Public_property_watermark_opacity" type="string"/>
		<property name="Watermark size" nameLabel="Image_Viewer_Public_property_watermark_size" type="string"/>
		<property name="Show frame" nameLabel="Image_Viewer_Public_property_show_frame" type="string"/>
		<property name="Animation" nameLabel="Image_Viewer_Public_property_animation" type="{None, Linear, Fading}"/>
		<property name="Correct frames" nameLabel="Image_Viewer_Public_property_correct_frames" type="string"/>
		<property name="Do not reset" nameLabel="Image_Viewer_Public_property_do_not_reset" type="boolean"/>
		<property name="Random frame" nameLabel="Image_Viewer_Public_property_random_frame" type="boolean"/>
		<property name="Initial frame" nameLabel="Image_Viewer_Public_property_initial_frame" type="string"/>
		<property name="Show frame counter" nameLabel="Image_Viewer_Public_property_show_frame_counter" type="boolean"/>
		<property name="langAttribute" nameLabel="Image_Viewer_Public_property_lang_attribute" type="string"/>
		<property name="Base width" nameLabel="Image_Viewer_Public_property_base_width" type="string"/>
		<property name="Base height" nameLabel="Image_Viewer_Public_property_base_height" type="string"/>
	</model>
<css>.image-viewer {
    border: none;
    padding: 0;
    margin: 0;
    float: left;
    overflow: hidden;
    background: no-repeat 0 0;
    -ms-touch-action: none;
}

.image-viewer-audio-wrapper {
    display: none;
}

.image-viewer-audio-wrapper audio {
    display: inherit;
}

.image-viewer-hidden-image {
    display: none;
}

.image-viewer-watermark,
.image-viewer-loading-image {
    border: 0;
    margin: 0;
    padding: 0;
    display: none;
    position: absolute;
}

.image-viewer-label {
    border: 0;
    margin: 0;
    padding: 0;
    position: absolute;
}

.image-viewer-loading-image {
    z-index: 10;
    width: 50px;
    height: 50px;
}

.image-viewer-watermark {
    display: block;
}

.frame-counter-wrapper {
    position: absolute;
    bottom: 5px;
    height: 10px;
    width: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
}

.frame-counter {
    height: 100%;
    margin-left: auto;
    margin-right: auto;
    background: none;
    display: table;
}

.dot {
    float: left;
    width: 10px;
    height: 10px;
    -webkit-border-radius: 5px;
    -moz-border-radius: 5px;
    border-radius: 5px;
    border: 1px solid black;
    background-color: #f0f8ff;
    margin: 0 1px;
    cursor: pointer;
}

.dot.current {
    background-color: #696969;
    cursor: default;
}
</css><view>&lt;div class='image-viewer'&gt;
    &lt;div class="image-viewer-helper"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="image-viewer-loading-image"/&gt;
&lt;div class="image-viewer-watermark"&gt;&lt;/div&gt;
&lt;div class="frame-counter-wrapper"&gt;
    &lt;div class="frame-counter"&gt;
        &lt;div class="dot current"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class='image-viewer'&gt;
    &lt;div class="image-viewer-helper"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img src="/media/images/loading.gif" alt="Loading..." class="image-viewer-loading-image"/&gt;
&lt;div class="image-viewer-watermark"&gt;&lt;/div&gt;
&lt;div class="frame-counter-wrapper"&gt;
    &lt;div class="frame-counter"&gt;
        &lt;div class="dot current"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonImage_Viewer_Public_create() {
    var presenter = function () {};

    var playerController = null;
    var watermarkElement;
    var loadingScreen = {
        element: null,
        counter: 0
    };
    var audioElements = [];

    presenter.addonId = null;
    presenter.upgradedModel = null;

    presenter.lastMovePosition = null;
    presenter.totalPercentage = 0;
    presenter.lastReceivedEvent = null;
    presenter.isMouseDown = false;
    presenter.eventBus = null;
    presenter.isWCAGSelected = false;

    presenter.ERROR_CODES = {
        'IM_01': "Image must be uploaded to display Addon!",
        'FN_01': "Frame number must be set to properly display Addon",
        'FN_02': "Frame number is not a number!",
        'FN_03': "Frame number must be a positive integer!",
        'FN_04': "Frame name cannot be empty!",
        'FN_05': "Frame names - frame number must be between 1 and frames count",
        'LA_01': "Label text cannot be empty!",
        'LA_02': "Label top value incorrect!",
        'LA_03': "Label left value incorrect!",
        'LA_04': "All label fields must be filled correctly!",
        'FL_01': "Label must be assigned to at least one frame!",
        'FL_02': "Frames list syntax incorrect! Check for separators!",
        'FL_03': "Frame number is invalid!",
        'FL_04': "Frame number missing inside list!",
        'FL_05': "Frame numbers range incorrect!",
        'WM_01': "Watermark color must be provided in #RRGGBB format!",
        'WM_02': "Watermark opacity must be a value from 0.0 to 1.0!",
        'WM_03': "Watermark size must be a positive integer number!",
        'CF_01': "Correct frame number must be between 1 and frames count!",
        'IF_01': "Initial frame is out of range. Please choose number between 1 and frames count.",
        'ID_01': "Base width and height must be either positive integers or empty"
    };

    presenter.FRAME_SIZE = {
        'Original': 'ORIGINAL',
        'Keep aspect ratio': 'SCALED',
        'Stretch': 'STRETCHED',
        DEFAULT: 'Original'
    };

    presenter.ANIMATION = {
        'None': 'NONE',
        'Linear': 'LINEAR',
        'Fading': 'FADING',
        DEFAULT: 'None'
    };

    presenter.configuration = {};

    presenter.changeFrameData = {
        isPreview: false,
        isReverseOrder: false,
        triggerEvent: false
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeFrom_01(model);
        upgradedModel =  presenter.upgradeFrom_02(upgradedModel);
        upgradedModel = presenter.upgradeAddAltTexts(upgradedModel);
        upgradedModel = presenter.upgradeAddBaseDimensions(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["Do not reset"]) {
            upgradedModel["Do not reset"] = false;
        }

        return upgradedModel;
    };

    presenter.upgradeFrom_02 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["Random frame"]) {
            upgradedModel["Random frame"] = false;
        }

        if (!upgradedModel["Initial frame"]) {
            upgradedModel["Initial frame"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeAddAltTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = "";
        }

        if (!upgradedModel["Alternative texts"]) {
            upgradedModel["Alternative texts"] =
                [{
                    "Alternative text": "",
                    "frame": ""
                }];
        }

        return upgradedModel;
    };

    presenter.upgradeAddBaseDimensions = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["Base width"]) {
            upgradedModel["Base width"] = "";
        }

        if (!upgradedModel["Base height"]) {
            upgradedModel["Base height"] = "";
        }

        return upgradedModel;
    };

    function showErrorMessage(element, errorCode) {
        $(element).html(presenter.ERROR_CODES[errorCode]);
    }

    function setContainerDimensions(wrapper) {
        var elementReducedSize = DOMOperationsUtils.calculateReducedSize(wrapper, presenter.$element);
        $(presenter.$element).css({
            width: elementReducedSize.width,
            height: elementReducedSize.height
        });

        presenter.configuration.containerDimensions = {
            width: elementReducedSize.width,
            height: elementReducedSize.height
        };
    }

    function loadSounds() {
        // Check for browser audio tag support
        if (!buzz.isSupported()) {
            return;
        }

        buzz.defaults.autoplay = false;
        buzz.defaults.loop = false;
        showLoadingScreen();

        for (var i = 0; i &lt; presenter.configuration.frames; i++) {
            if ((i &gt; presenter.configuration.sounds.length - 1 ) || presenter.configuration.sounds[i].isEmpty) {
                audioElements[i] = null;
            } else {
                if (presenter.configuration.sounds[i].MP3 !== "" &amp;&amp; buzz.isMP3Supported()) {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].MP3);
                } else if (presenter.configuration.sounds[i].OGG !== "" &amp;&amp; buzz.isOGGSupported()) {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].OGG);
                } else {
                    audioElements[i] = new buzz.sound(presenter.configuration.sounds[i].AAC);
                }

                audioElements[i].load();
            }
        }

        hideLoadingScreen();
    }

    function hideWatermarkIfVisible() {
        if ($(watermarkElement).is(':visible')) {
            $(watermarkElement).hide();
        }
    }

    function showWatermarkIfNotVisible() {
        if ($(watermarkElement).not(':visible')) {
            $(watermarkElement).show();
        }
    }

    function clickHandler(e) {
        if (e)
            e.stopPropagation();

        if (presenter.configuration.isClickDisabled) return;
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) return;
        if(presenter.isShowAnswersActive) return;

        if (presenter.mouseData.isMouseDragged) {
            presenter.mouseData.isMouseDragged = false;
            return;
        }

        hideWatermarkIfVisible();

        presenter.next();
    }

    function isMoreThanOneFingerGesture(event) {
        var touch, touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];
        if (event.hasOwnProperty('touches'))
            touchPoints = event.touches
        return touchPoints.length&gt; 1;
    }

    function touchStartCallback (event) {
        if (isMoreThanOneFingerGesture(event)) return;

        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0]
        else
            touch = touchPoints[0];
        mouseDownCallback(touch);
    }

    function getFixedBackgroundPosition() {
        return parseInt(presenter.$element.css('backgroundPosition'), 10);
    }

    function mouseDownCallback(event) {
        if (presenter.configuration.isClickDisabled) return;
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) return;
        if(presenter.isShowAnswersActive) return;

        presenter.mouseData.isMouseDown = true;
        presenter.mouseData.oldPosition.x = event.pageX;
        presenter.mouseData.oldPosition.y = event.pageY;
    }

    function touchEndCallback () {
        mouseUpCallback();
    }

    function getRequiredShift() {
        return (presenter.$element.width() / 100) * 30; // 30% of frame width
    }

    function mouseUpCallback() {
        if (presenter.configuration.isClickDisabled) return;
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) {
            presenter.mouseData.isMouseDown = false;
            delete presenter.mouseData.originalBackgroundPosition;
            return;
        }
        if(presenter.isShowAnswersActive) return;

        var originalAnimation = presenter.configuration.animation;
        presenter.configuration.animation = presenter.ANIMATION.Linear;

        if (presenter.mouseData.isMouseDragged) {
            var currentBackgroundPosition = getFixedBackgroundPosition(),
                shift = Math.abs(currentBackgroundPosition - presenter.mouseData.originalBackgroundPosition),
                isNextFrameShift = currentBackgroundPosition - presenter.mouseData.originalBackgroundPosition &lt; 0;

            // Restoring percentage calculation of background position
            var newPosition = Math.abs(currentBackgroundPosition) / (presenter.configuration.backgroundImageWidth - presenter.$element.width());
            newPosition *= 100;
            presenter.$element.css('backgroundPosition', newPosition + '%');

            if (shift &gt; getRequiredShift()) {
                var currentFrame = presenter.configuration.currentFrame;
                var framesCount = presenter.configuration.frames;

                if (isNextFrameShift) {
                    if (currentFrame &lt; framesCount - 1) {
                        presenter.next();
                    } else {
                        presenter.configuration.currentFrame = 0;
                        presenter.$element.css({
                            'background-position':'-' + presenter.configuration.frameWidthPercentage + '%'
                        });
                        presenter.$element.animate({
                            'background-position':'0%'
                        }, 500, "linear");
                        presenter.changeFrameLogic(false, true);
                    }
                } else {
                    if (currentFrame !== 0) {
                        presenter.previous();
                    } else {
                        presenter.configuration.currentFrame = framesCount - 1;
                        presenter.$element.css({
                            'background-position':(100 + presenter.configuration.frameWidthPercentage) + '%'
                        });
                        presenter.$element.animate({
                            'background-position':'100%'
                        }, 500, "linear")
                        presenter.changeFrameLogic(false, true);
                    }
                    presenter.configuration.currentFrame = currentFrame &gt; 1 ? currentFrame - 1 : framesCount - 1;
                }

            } else {
                var isReverseOrder = presenter.configuration.currentFrame === 0;
                presenter.changeFrame(false, isReverseOrder, false, false);
            }
        }

        presenter.configuration.animation = originalAnimation;
        presenter.mouseData.isMouseDown = false;
        delete presenter.mouseData.originalBackgroundPosition;

        if (!MobileUtils.isWindowsMobile(window.navigator) &amp;&amp; presenter.isMobileDevice) {
            presenter.mouseData.isMouseDragged = false;
        }
        if (MobileUtils.isWindowsMobile(window.navigator) &amp;&amp; shift &lt;= getRequiredShift()) {
            presenter.mouseData.isMouseDragged = false;
            clickHandler(false);
            presenter.mouseData.isMouseDragged = true;
        }
    }

    function touchMoveCallback (event) {
        if (isMoreThanOneFingerGesture(event)) return;

        event.preventDefault();

        var touch;
        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0]
        else
            touch = touchPoints[0];
        mouseMoveCallback(touch);
    }

    function mouseMoveCallback(event) {
        if (presenter.configuration.isClickDisabled) return;
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) return;
        if(presenter.isShowAnswersActive) return;

        if (presenter.mouseData.isMouseDown) {
            presenter.mouseData.isMouseDragged = true;

            if (presenter.$element.css('background-position').indexOf('px') == -1) {
                var oldPositionInPx = (-1 * presenter.configuration.currentFrame) * presenter.$element.width();
                presenter.$element.css('backgroundPosition', oldPositionInPx + 'px');
                presenter.mouseData.originalBackgroundPosition = oldPositionInPx;
            }

            var relativeDistance = event.pageX - presenter.mouseData.oldPosition.x,
                oldPosition = parseInt(presenter.$element.css('backgroundPosition'), 10);
            presenter.$element.css('backgroundPosition', (oldPosition + relativeDistance) + 'px');

            presenter.mouseData.oldPosition.x = event.pageX;
            presenter.mouseData.oldPosition.y = event.pageY;
        }
    }

    /**
     * Calculate scale for image containing element depending on frame aspect ratio
     *
     * @return {Object} calculated horizontal and vertical scale
     */
    presenter.calculateContainerDimensions = function(imageWidth, imageHeight, containerWidth, containerHeight) {
        var imageRatio = imageWidth / imageHeight;
        var containerRatio = containerWidth / containerHeight;

        var horizontal = imageRatio &gt;= containerRatio ? containerWidth : containerHeight * imageRatio;
        var vertical = imageRatio &gt;= containerRatio ? containerWidth / imageRatio : containerHeight;

        return {
            horizontal: horizontal,
            vertical: vertical
        };
    };


    // This function returns string containing CSS declaration of elements
    // background image size in percentage measure
    function calculateBackgroundSize(size, framesCount) {
        var cssValue;

        switch(size) {
            case 'SCALED':
            case 'STRETCHED':
                cssValue = framesCount * 100;
                cssValue += '% 100%';
                break;
            default:
                cssValue = '';
        }

        return cssValue;
    }

    function shouldShowWatermark() {
        return presenter.configuration.showWatermark &amp;&amp; !presenter.configuration.isClickDisabled;
    }

    function calculateWidth (containerWidth, imageWidth) {
        var differenceContainerElement = containerWidth - (imageWidth / presenter.configuration.frames);
        var differenceImage = differenceContainerElement * presenter.configuration.frames;

        var scaleWidth = imageWidth + differenceImage;

        return {
            scaleWidth: scaleWidth,
            differenceContainerElement: differenceContainerElement,
            elementWidth: (imageWidth / presenter.configuration.frames) + differenceContainerElement
        }
    }

    function loadImage(preview) {
        var tempImageElement = document.createElement('img');
        $(tempImageElement).addClass('image-viewer-hidden-image');
        $(tempImageElement).attr('src', presenter.configuration.imageSrc);
        $(presenter.$view).append(tempImageElement);

        $(tempImageElement).load(function() {
            var containerWidth = presenter.configuration.containerDimensions.width;
            var containerHeight = presenter.configuration.containerDimensions.height;
            var imageWidth = $(this).width();
            var imageHeight = $(this).height();
            var containerDimensions = presenter.calculateContainerDimensions(imageWidth / presenter.configuration.frames, imageHeight, containerWidth, containerHeight);
            var backgroundSize = calculateBackgroundSize(presenter.configuration.frameSize, presenter.configuration.frames);

            var elementWidth;
            var elementHeight;

            switch (presenter.configuration.frameSize) {
                case 'ORIGINAL':
                    elementWidth = imageWidth / presenter.configuration.frames;
                    elementHeight = imageHeight;
                    break;
                case 'SCALED':
                    var calculatedWidthScale = calculateWidth(containerWidth, imageWidth);
                    $(this).width(calculatedWidthScale.scaleWidth);

                    elementWidth = calculatedWidthScale.elementWidth;
                    elementHeight = containerDimensions.vertical;
                    break;
                case 'STRETCHED':
                    var calculatedWidthStretched = calculateWidth(containerWidth, imageWidth);
                    $(this).width(calculatedWidthStretched.scaleWidth);

                    elementWidth = calculatedWidthStretched.elementWidth;
                    elementHeight = containerHeight;
                    break;
            }

            presenter.$element.css({
                width: elementWidth + 'px',
                height: elementHeight + 'px',
                backgroundImage: "url('" + presenter.configuration.imageSrc + "')"
            });
            presenter.$elementHelper.css({
                width: elementWidth + 'px',
                height: elementHeight + 'px'
            });

            if (backgroundSize) {
                $(presenter.$element).css('background-size', backgroundSize);
            }

            presenter.configuration.backgroundImageWidth = $(this).width();

            $(this).remove();
            presenter.changeFrame(true, false, false, false);

            if (shouldShowWatermark()) {
                var watermarkOptions = {
                    "color": presenter.model["Watermark color"],
                    "opacity": presenter.model["Watermark opacity"],
                    "size": presenter.model["Watermark size"]
                };
                Watermark.draw(watermarkElement, watermarkOptions);
            }

            hideLoadingScreen();
            presenter.$view.trigger("onLoadImageEnd", [preview]);
        });
    }

    function prepareLoadingScreen(containerWidth, containerHeight) {
        $(loadingScreen.element).css({
            top: ((containerHeight - $(loadingScreen.element).height()) / 2) + 'px',
            left: ((containerWidth - $(loadingScreen.element).width()) / 2) + 'px'
        });
    }

    function showLoadingScreen() {
        $(loadingScreen.element).show();
        loadingScreen.counter++;
    }

    function hideLoadingScreen() {
        loadingScreen.counter--;
        if (loadingScreen.counter === 0) {
            $(loadingScreen.element).hide();
        }
    }

    /**
     * Creates label SPAN element and appends it to Addon space
     *
     * @param label
     * @return newly created element DOM reference
     */
    function createLabelElement(label) {
        var labelElement = document.createElement('span');
        var scaleX = 1.0;
        var scaleY = 1.0;
        if (presenter.configuration.baseDimensions.width != 0) {
            scaleX = presenter.configuration.containerDimensions.width / presenter.configuration.baseDimensions.width;
        }
         if (presenter.configuration.baseDimensions.height != 0) {
            scaleY = presenter.configuration.containerDimensions.height / presenter.configuration.baseDimensions.height;
        }

        $(labelElement).addClass('image-viewer-label');
        $(labelElement).html(label.text);
        if (scaleX != 1.0 || scaleY != 1.0) {
            $(labelElement).css(generateTransformDict(scaleX, scaleY));
        }
        $(labelElement).css({
            top: (label.top * scaleY) + 'px',
            left: (label.left * scaleX) + 'px'
        });

        $(presenter.$view).append(labelElement);

        return labelElement;
    }

    function generateTransformDict(scaleX, scaleY) {
        var scale = "scale(" + scaleX + "," + scaleY + ")";
        return {
            'transform': scale,
            '-ms-transform': scale,
            '-webkit-transform': scale,
            '-o-transform': scale,
            '-moz-transform': scale,
           "-webkit-transform-origin": "top left",
           "-ms-transform-origin": "top left",
           "transform-origin": "top left"
        }
    }

    function loadLabels() {
        for (var i = 0; i &lt; presenter.configuration.labels.length; i++) {
            presenter.configuration.labels[i].element = createLabelElement(presenter.configuration.labels[i]);
        }
    }

    presenter.hideLabels = function () {
        $(presenter.$view).find('.image-viewer-label').css('visibility', 'hidden');
    };

    /**
     * Displays labels that are assigned to given frame.
     *
     * @param frame frame number counted from 1 to n
     */
    presenter.displayLabels = function (frame) {
        $(presenter.$view).find('.image-viewer-label').css('visibility', 'hidden');
        for (var i = 0; i &lt; presenter.configuration.labels.length; i++) {
            var label = presenter.configuration.labels[i];
            for (var j = 0; j &lt; label.frames.length; j++) {
                if (frame === label.frames[j]) {
                    $(label.element).css('visibility', 'visible');
                }
            }
        }
    };

    function attachEventHandlers() {

        presenter.isMobileDevice = MobileUtils.isMobileUserAgent(navigator.userAgent) || MobileUtils.isEventSupported('touchend');
        if (MobileUtils.isWindowsMobile(window.navigator)) {
            presenter.$element[0].addEventListener("MSPointerDown", touchStartCallback, false);
            presenter.$element[0].addEventListener("MSPointerUp", touchEndCallback, false);
            presenter.$element[0].addEventListener("MSPointerMove", touchMoveCallback, false);
        }
        else if (presenter.isMobileDevice) {
            presenter.$element[0].ontouchstart = touchStartCallback;
            presenter.$element[0].ontouchend = touchEndCallback;
            presenter.$element[0].ontouchmove = touchMoveCallback;
        }
        else {
            presenter.$element.mousedown(mouseDownCallback);
            presenter.$element.mouseup(mouseUpCallback);
            presenter.$element.mousemove(mouseMoveCallback);
        }

        presenter.$element.click(clickHandler);

        $(watermarkElement).click(function(e) {
            e.stopPropagation();

            $(watermarkElement).hide();
            clickHandler(e);
        });

        if (presenter.configuration.showFrameCounter) {
            var $frameCounter = presenter.$view.find('.frame-counter:first');

            $frameCounter.find('.dot').each(function (index) {
                $(this).click(function (e) {
                    e.stopPropagation();

                    if (presenter.configuration.isClickDisabled) return;
                    if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) return;
                    if (index === presenter.configuration.currentFrame) return;

                    hideWatermarkIfVisible();

                    presenter.moveToFrame(index + 1);
                });
            });
        }
    }

    function loadImagesCallback(preview) {
        if (!presenter.configuration.correctFrames.isExerciseMode) {
            presenter.getMaxScore = undefined;
            presenter.getScore = undefined;
            presenter.getErrorCount = undefined;
        }

        if (!preview) {
            loadSounds();

            if (!presenter.configuration.isClickDisabled) {
                attachEventHandlers();
            }
        }
        presenter.imageLoadedDeferred.resolve();
    }

    function loadImageEndCallback() {
        var configuration = presenter.configuration;

        if(!configuration.showWatermark) {
            $(watermarkElement).remove();
        }
        presenter.setVisibility(configuration.currentVisibility);
        presenter.changeFrame(presenter.changeFrameData.isPreview, presenter.changeFrameData.isReverseOrder, presenter.changeFrameData.triggerEvent, false);
    }

    function presenterLogic(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.imageLoadedDeferred = new jQuery.Deferred();
        presenter.imageLoaded = presenter.imageLoadedDeferred.promise();
        presenter.addonId = upgradedModel.ID;
        presenter.$view = $(view);
        presenter.model = upgradedModel;
        presenter.isPreview = isPreview;
        presenter.$element = $(presenter.$view.find('.image-viewer:first')[0]);
        presenter.$elementHelper = $(presenter.$view.find('.image-viewer-helper:first')[0]);
        loadingScreen.element = presenter.$view.find('.image-viewer-loading-image:first')[0];
        watermarkElement = presenter.$view.find('.image-viewer-watermark:first')[0];

        if (!isPreview) {
            var loadingSrc = DOMOperationsUtils.getResourceFullPath(playerController, "media/loading.gif");
            if (loadingSrc) $(loadingScreen.element).attr('src', loadingSrc);
        }

        var configuration = presenter.validateModel(upgradedModel);
        if (configuration.isError) {
            showErrorMessage(view, configuration.errorCode);
        } else {
            showLoadingScreen();

            presenter.configuration = configuration;
            presenter.configuration.frameWidthPercentage = 100 / (presenter.configuration.frames - 1);
            presenter.configuration.isErrorMode = false;

            // Initialize mouse data
            presenter.mouseData = {
                isMouseDown : false,
                oldPosition : { x: 0, y: 0 },
                isMouseDragged : false
            };

            if (isPreview) {
                presenter.configuration.currentFrame = presenter.configuration.showFrame &lt;= presenter.configuration.frames ? presenter.configuration.showFrame - 1 : 0;
            } else {
                presenter.setCurrentFrame();
            }

            setContainerDimensions(view);
            prepareLoadingScreen(upgradedModel.Width, upgradedModel.Height);
            presenter.adjustFrameCounter();
            loadLabels();

            presenter.$view.bind("onLoadImageEnd", function (event, isPreview) {
                loadImagesCallback(isPreview);
            });
            if(!isPreview){
                presenter.pageLoaded.then(function() {
                    loadImage(isPreview);
                });
            }else{
                loadImage(isPreview);
            }
            presenter.setVisibility(presenter.configuration.defaultVisibility || isPreview);
            if (presenter.configuration.defaultVisibility) {
                presenter.displayLabels(1);
            }

            if (!configuration.showWatermark) {
                hideWatermarkIfVisible();
            }
        }
    }

    presenter.adjustFrameCounter = function () {
        var $wrapper = presenter.$view.find('.frame-counter-wrapper:first'), i, $dot,
            $frameCounter = $wrapper.find('.frame-counter:first');

        if (!presenter.configuration.showFrameCounter) {
            $wrapper.remove();
            return;
        }

        for (i = 0; i &lt; presenter.configuration.frames - 1; i++) {
            $dot = $(document.createElement('div'));
            $dot.addClass('dot');
            $frameCounter.append($dot);
        }
    };

    presenter.changeCurrentDot = function () {
        var $frameCounter = presenter.$view.find('.frame-counter:first');

        $frameCounter.find('.dot.current').removeClass('current');
        $frameCounter.find('.dot:eq(' + presenter.configuration.currentFrame + ')').addClass('current');
    };

    presenter.setCurrentFrame = function() {
        if(presenter.configuration.initialFrame) {
            presenter.configuration.currentFrame = presenter.configuration.initialFrame - 1;
        } else if(presenter.configuration.isRandomFrame) {
            presenter.configuration.currentFrame = presenter.getRandomFrame();
        } else {
            presenter.configuration.currentFrame = 0;
        }
    };

    presenter.next = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var currentFrame = presenter.configuration.currentFrame,
            framesCount = presenter.configuration.frames;

        presenter.configuration.shouldCalcScore = true;
        presenter.configuration.currentFrame = currentFrame === framesCount - 1 ? 0 : currentFrame + 1;
        presenter.changeFrame(false, false, true, true);
    };

    presenter.previous = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var currentFrame = presenter.configuration.currentFrame,
            framesCount = presenter.configuration.frames;

        presenter.configuration.shouldCalcScore = true;
        presenter.configuration.currentFrame = currentFrame === 0 ? framesCount - 1 : currentFrame - 1;
        presenter.changeFrame(false, true, true, true);
    };

    presenter.isValidFrameNumber = function(frame, framesCount) {
        return !isNaN(frame) &amp;&amp; frame &gt; 0 &amp;&amp; frame &lt;= framesCount;
    };

    presenter.moveToFrameCommand = function(params) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var framesCount = presenter.configuration.frames,
            currentFrame = presenter.configuration.currentFrame,
            validatedFrame = ModelValidationUtils.validateIntegerInRange(params[0], framesCount, 1);

        presenter.configuration.shouldCalcScore = true;

        if (validatedFrame.isValid &amp;&amp; validatedFrame.value - 1 !== currentFrame) {
            var isReverseOrder = currentFrame &gt; validatedFrame.value - 1;
            presenter.configuration.currentFrame = validatedFrame.value - 1;

            presenter.changeFrameData = {
                isPreview: false,
                isReverseOrder: isReverseOrder,
                triggerEvent: true
            };

            $.when(presenter.imageLoaded).then(loadImageEndCallback);
            presenter.readFrame(presenter.configuration.currentFrame);
        }
    };

    presenter.moveToFrame = function (frame) {
        presenter.moveToFrameCommand([frame]);
    };

    presenter.isValidFrameName = function (frameName) {
        var isInvalid = !frameName || frameName === "";

        return !isInvalid;
    };

    presenter.findFrame = function (name, frames) {
        for (var i = 0, length = frames.length; i &lt; length; i++) {
            if (frames[i].name == name) {
                return {
                    found: true,
                    frameNumber: parseInt(frames[i].frame, 10)
                }
            }
        }

        return {
            found: false
        };
    };

    presenter.moveToFrameNameCommand = function (params) {
        var frameFindResult, frameNumber;

        presenter.configuration.shouldCalcScore = true;

        if (!presenter.isValidFrameName(params[0])) return;

        frameFindResult = presenter.findFrame(params[0], presenter.configuration.frameNames);
        if (!frameFindResult.found) return;

        frameNumber = frameFindResult.frameNumber;
        if (frameNumber - 1 !== presenter.configuration.currentFrame) {
            var currentFrame = presenter.configuration.currentFrame;
            var isReverseOrder = currentFrame &gt; frameNumber - 1;
            presenter.configuration.currentFrame = frameNumber - 1;
            presenter.changeFrame(false, isReverseOrder, true, true);
        }
    };

    presenter.moveToFrameName = function (frameName) {
        presenter.moveToFrameNameCommand([frameName]);
    };

    presenter.getRandomFrame = function() {
        var frames = presenter.configuration.frames;
        return Math.floor( Math.random() * frames );
    };

    presenter.setClickDisabled = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isClickDisabled = true;
        hideWatermarkIfVisible();
    };
    presenter.setClickEnabled = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isClickDisabled = false;
        if (presenter.configuration.showWatermarkbyDefault) {
            showWatermarkIfNotVisible();
        }
    };

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.correctFrames.isExerciseMode) return;

        var commands = {
            'next': presenter.next,
            'previous': presenter.previous,
            'moveToFrame': presenter.moveToFrameCommand,
            'moveToFrameName': presenter.moveToFrameNameCommand,
            'getCurrentFrame': presenter.getCurrentFrame,
            'show': presenter.show,
            'hide': presenter.hide,
            'markAsCorrect': presenter.markAsCorrect,
            'markAsWrong': presenter.markAsWrong,
            'setClickDisabled': presenter.setClickDisabled,
            'setClickEnabled': presenter.setClickEnabled
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.configuration.shouldCalcScore = true;
        presenter.setVisibility(true);
        presenter.displayLabels(presenter.configuration.currentFrame + 1);
        presenter.configuration.currentVisibility = true;
    };

    presenter.hide = function() {
        presenter.configuration.shouldCalcScore = true;
        presenter.setVisibility(false);
        presenter.hideLabels();
        presenter.configuration.currentVisibility = false;
    };

    presenter.markAsCorrect = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.$element.addClass('correct');
        presenter.$element.removeClass('wrong');
    };

    presenter.markAsWrong = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.$element.addClass('wrong');
        presenter.$element.removeClass('correct');
    };

    presenter.getCurrentFrame = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        return presenter.configuration.currentFrame + 1;
    };

    presenter.createPreview = function(view, model){
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();

        presenterLogic(view, model, false);

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.validateAnimation = function (animation) {
        return ModelValidationUtils.validateOption(presenter.ANIMATION, animation);
    };

    presenter.validateFrameSize = function(size) {
        return ModelValidationUtils.validateOption(presenter.FRAME_SIZE, size);
    };

    presenter.validateImage = function(imageSrc) {
        if (ModelValidationUtils.isStringWithPrefixEmpty(imageSrc, '/file/')) {
            return { isError : true, errorCode: "IM_01" };
        }

        return { isError: false, image : imageSrc };
    };

    presenter.validateFrameNumber = function(frame) {
        if (!frame) return { isError: true, errorCode: "FN_01" };

        var parsedFrame = parseInt(frame, 10);

        if (isNaN(parsedFrame)) return { isError:true, errorCode:"FN_02" };
        if (parsedFrame &lt; 1) return { isError:true, errorCode:"FN_03" };

        return { isError: false, frames : parsedFrame };
    };

    presenter.validateAlternativeTexts = function (altTexts, framesNumber) {
        var parsedAltTexts = [];

        for (var i = 0; i &lt; framesNumber; i++) parsedAltTexts.push("");

        if (altTexts.length != 1 || altTexts[0]["Alternative text"] != "") {
            for (var i = 0; i &lt; altTexts.length; i++) {
                var alt = altTexts[i];
                var parsedNumber = presenter.validateFrameNumber(alt.frame);
                if (parsedNumber.isError) return parsedNumber;
                parsedAltTexts[parsedNumber.frames - 1] = alt["Alternative text"];
            }
        }
        return { isError: false, altTexts: parsedAltTexts };

    };

    presenter.validateSound = function(soundsArray) {
        var sounds = [];

        if (soundsArray &amp;&amp; $.isArray(soundsArray)) {
            for (var i = 0; i &lt; soundsArray.length; i++) {
                var isMP3Empty = ModelValidationUtils.isStringWithPrefixEmpty(soundsArray[i]['MP3 sound'], '/file/');
                var isAACEmpty = ModelValidationUtils.isStringWithPrefixEmpty(soundsArray[i]['AAC sound'], '/file/');
                var isOGGEmpty = ModelValidationUtils.isStringWithPrefixEmpty(soundsArray[i]['OGG sound'], '/file/');
                var isEmpty = isMP3Empty &amp;&amp; isAACEmpty &amp;&amp; isOGGEmpty;

                sounds.push({
                    AAC : isAACEmpty ? "" : soundsArray[i]['AAC sound'],
                    OGG : isOGGEmpty ? "" : soundsArray[i]['OGG sound'],
                    MP3 : isMP3Empty ? "" : soundsArray[i]['MP3 sound'],
                    isEmpty : isEmpty
                });
            }
        }

        return {
            sounds : sounds
        };
    };

    presenter.validateFrameNames = function(frameNames, frames) {
        var frameNamesArray = [];

        var frameNamesEmpty = false;
        if (frameNames &amp;&amp; $.isArray(frameNames)) {
            var firstName = frameNames[0].name;
            var firstFrame = frameNames[0].frame;
            if (firstFrame === "" &amp;&amp; firstName === "") {
                frameNamesEmpty = true;
            } else {
                for (var j = 0; j &lt; frameNames.length; j++) {
                    var frame = frameNames[j]['frame'];
                    var name = frameNames[j]['name'];

                    if (!name || name === null || name === "") {
                        return {
                            isError: true,
                            errorCode: "FN_04"
                        };
                    }

                    var validatedFrame = presenter.validateFrameNumber(frame);
                    if (validatedFrame.isError) {
                        return {
                            isError: true,
                            errorCode: validatedFrame.errorCode
                        };
                    }

                    if (validatedFrame.frames &gt; frames) {
                        return {
                            isError: true,
                            errorCode: "FN_05"
                        };
                    }

                    frameNamesArray.push({
                        name: name,
                        frame: frame
                    });
                }
            }
        }

        return {
            isError: false,
            frameNames: frameNamesArray,
            frameNamesEmpty: frameNamesEmpty
        };
    };

    /**
     * This function validates and converts number from string representation to integer value
     */
    presenter.validatePosition = function (position) {
        var validatedPosition = ModelValidationUtils.validateInteger(position);

        if (!validatedPosition.isValid) {
            return { isError:true };
        }

        if (validatedPosition.value &lt; 0) {
            return { isError:true };
        }

        return {
            isError: false,
            position: validatedPosition.value
        };
    };

    /**
     * Removes duplicates from sorted array of numbers
     *
     * @param array sorted array of numbers
     * @return array with removed duplicates
     */
    presenter.removeDuplicatesFromArray = function(array) {
        if (array.length === 0) {
            return [];
        }

        var results = [];

        for (var i = 0; i &lt; array.length - 1; i++) {
            if (array[i + 1] !== array[i]) {
                results.push(array[i]);
            }
        }

        results.push(array[array.length - 1]);

        return results;
    };

    /**
     * Validates frames numbers list separated with commas
     *
     * @param frames string representation of frames list
     * @param count frames count
     *
     * @return array of frames numbers counted from 1 to n
     */
    presenter.validateFramesList = function (frames, count) {
        var list = [];

        if (ModelValidationUtils.isStringEmpty(frames)) {
            return {
                isError: true,
                errorCode: "FL_01"
            };
        }

        var regExp = new RegExp('[0-9a-zA-Z\,\-]+'); // Only digits and commas are allowed in slides list
        var matchResult = frames.match(regExp);
        if (matchResult === null || frames.length !== matchResult[0].length) {
            return {
                isError: true,
                errorCode: "FL_02"
            };
        }

        var splittedFrames = frames.split(',');
        for (var i = 0; i &lt; splittedFrames.length; i++) {
            if (ModelValidationUtils.isStringEmpty(splittedFrames[i])) {
                return {
                    isError: true,
                    errorCode: "FL_04"
                };
            }

            var indexOfRange = splittedFrames[i].search('-');
            if (indexOfRange !== -1) {
                var rangeEnd = splittedFrames[i].split('-')[1];
                var validatedRangeEnd = ModelValidationUtils.validateIntegerInRange(rangeEnd, count, 1);
                if (!validatedRangeEnd.isValid) {
                    return {
                        isError: true,
                        errorCode: "FL_05"
                    };
                }

                var rangeStart = splittedFrames[i].split('-')[0];
                var validatedRangeStart = ModelValidationUtils.validateIntegerInRange(rangeStart, rangeEnd.value, 1);
                if (!validatedRangeStart.isValid || validatedRangeStart.value &gt; validatedRangeEnd.value) {
                    return {
                        isError: true,
                        errorCode: "FL_05"
                    };
                }

                for (var frameNumber = validatedRangeStart.value; frameNumber &lt;= validatedRangeEnd.value; frameNumber++) {
                    list.push(frameNumber);
                }

                continue;
            }

            var validatedFrame = ModelValidationUtils.validateIntegerInRange(splittedFrames[i], count, 1);
            if (!validatedFrame.isValid) {
                return {
                    isError: true,
                    errorCode: "FL_03"
                };
            }

            list.push(validatedFrame.value);
        }

        list = list.sort();
        list = presenter.removeDuplicatesFromArray(list);

        return {
            isError: false,
            list: list
        };
    };

    presenter.validateLabels = function (labelsArray, framesCount) {
        var labels = [], element;

        // Ugly fix for Editor problems
        if (labelsArray === undefined) return { isError:false, labels:labels };

        for (var i = 0; i &lt; labelsArray.length; i++) {
            element = {
                Text: labelsArray[i].Text,
                Frames: labelsArray[i].Frames,
                Top: labelsArray[i].Top,
                Left: labelsArray[i].Left
            };

            if (ModelValidationUtils.isArrayElementEmpty(element)) {
                if (labelsArray.length == 1)
                    return { isError:false, labels:labels };
                else
                    return { isError:true, errorCode:"LA_04" };
            }

            if (ModelValidationUtils.isStringEmpty(element.Text)) {
                return { isError:true, errorCode:"LA_01" };
            }

            var validatedFramesList = presenter.validateFramesList(element.Frames, framesCount);
            if (validatedFramesList.isError) return { isError:true, errorCode: validatedFramesList.errorCode };

            var validatedTopPosition = ModelValidationUtils.validateInteger(element.Top);
            if (!validatedTopPosition.isValid) return { isError:true, errorCode:"LA_02" };

            var validatedLeftPosition = ModelValidationUtils.validateInteger(element.Left);
            if (!validatedLeftPosition.isValid) return { isError:true, errorCode:"LA_03" };

            var text = {
                text: element.Text,
                frames: validatedFramesList.list,
                top: validatedTopPosition.value,
                left: validatedLeftPosition.value
            };

            labels.push(text);
        }

        return { isError:false, labels:labels };
    };

    presenter.validateInitialFrame = function(frame, framesCount) {
        if(frame &amp;&amp; !ModelValidationUtils.validateIntegerInRange(frame, framesCount, 1).isValid) {
            return {
                isError: true,
                errorCode: 'IF_01'
            }
        }
        var isValid = this.isValidFrameNumber(frame, framesCount);
        return isValid ? frame : false;
    };

    presenter.validateOpacity = function (opacity) {
        if (ModelValidationUtils.isStringEmpty(opacity)) return { isError: false, opacity: 1.0 };

        var parsedOpacity = ModelValidationUtils.validateFloatInRange(opacity, 1.0, 0.0, 2);
        if (!parsedOpacity.isValid) return { isError: true };

        return { isError: false, opacity: parsedOpacity.value };
    };

    presenter.validateCorrectFrame = function (frames, framesCount) {
        if (ModelValidationUtils.isStringEmpty(frames)) return { isExerciseMode: false };

        var framesArray = frames.split(',');
        var correctFrames = [];

        for (var i = 0, length = framesArray.length; i &lt; length; i++) {
            var frameNumber = ModelValidationUtils.validateInteger(framesArray[i]);
            if (!frameNumber.isValid) return { errorCode: "FN_02" };

            frameNumber = ModelValidationUtils.validateIntegerInRange(framesArray[i], framesCount, 1);
            if (!frameNumber.isValid) return { errorCode: "CF_01" };

            correctFrames.push(frameNumber.value - 1);
        }

        return { frames: correctFrames, isExerciseMode: true };
    };

    presenter.validateModel = function(model) {
        var validatedImage = presenter.validateImage(model.Image);
        if (validatedImage.isError) return { isError: true, errorCode: validatedImage.errorCode };

        var validatedFrames = presenter.validateFrameNumber(model.Frames);
        if (validatedFrames.isError) return { isError: true, errorCode: validatedFrames.errorCode };

        var validatedFrameNames = presenter.validateFrameNames(model["Frame names"], validatedFrames.frames);
        if (validatedFrameNames.isError) return { isError: true, errorCode: validatedFrameNames.errorCode };

        var validatedLabels = presenter.validateLabels(model.Labels, validatedFrames.frames);
        if (validatedLabels.isError) return { isError: true, errorCode: validatedLabels.errorCode };

        var validatedCorrectFrames = presenter.validateCorrectFrame(model["Correct frames"], validatedFrames.frames);
        if (validatedCorrectFrames.errorCode) return { isError: true, errorCode: validatedCorrectFrames.errorCode };

        var showFrame = 1;
        var validatedShowFrame = ModelValidationUtils.validateIntegerInRange(model["Show frame"], validatedFrames.frames, 1);
        if (validatedShowFrame.isValid) {
            showFrame = validatedShowFrame.value;
        }

        var validatedInitialFrame = this.validateInitialFrame(model["Initial frame"], validatedFrames.frames);
        if(validatedInitialFrame.errorCode) return { isError: true, errorCode: validatedInitialFrame.errorCode };

        var validatedAltTexts = presenter.validateAlternativeTexts(model["Alternative texts"], validatedFrames.frames);
        if(validatedAltTexts.errorCode) return { isError: true, errorCode: validatedAltTexts.errorCode };

        var baseDimensions = {
            width: 0,
            height: 0
        };

        if (model['Base width'].trim().length !== 0) {
            var validatedBaseWidth = ModelValidationUtils.validatePositiveInteger(model['Base width']);
            if (validatedBaseWidth.isValid) {
                baseDimensions.width = validatedBaseWidth.value;
            } else {
                validatedBaseWidth.errorCode = 'ID_01';
                validatedBaseWidth.isError = true;
                return validatedBaseWidth;
            }
        }
        if (model['Base height'].trim().length !== 0) {
            var validatedBaseHeight = ModelValidationUtils.validatePositiveInteger(model['Base height']);
            if (validatedBaseHeight.isValid) {
                baseDimensions.height = validatedBaseHeight.value;
            } else {
                validatedBaseHeight.errorCode = 'ID_01';
                validatedBaseHeight.isError = true;
                return validatedBaseHeight;
            }
        }

        var validatedSound = presenter.validateSound(model.Sounds);
        var isClickDisabled = ModelValidationUtils.validateBoolean(model.isClickDisabled);
        var frameSize = presenter.validateFrameSize(model["Frame size"]);
        var animation = presenter.validateAnimation(model.Animation);
        var defaultVisibility = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        var isDoNotReset = ModelValidationUtils.validateBoolean(model["Do not reset"]);
        var isRandomFrame = ModelValidationUtils.validateBoolean(model["Random frame"]);

        return {
            isError: false,
            imageSrc : validatedImage.image,
            frames : validatedFrames.frames,
            sounds : validatedSound.sounds,
            frameNames: validatedFrameNames.frameNames,
            frameNamesEmpty: validatedFrameNames.frameNamesEmpty,
            isClickDisabled: isClickDisabled,
            isClickDisabledbyDefault: isClickDisabled,
            frameSize: frameSize,
            labels: validatedLabels.labels,
            showWatermark: ModelValidationUtils.validateBoolean(model["Show watermark"]),
            showWatermarkbyDefault: ModelValidationUtils.validateBoolean(model["Show watermark"]),
            showFrame: showFrame,
            animation: animation,
            correctFrames: validatedCorrectFrames,
            defaultVisibility: defaultVisibility,
            currentVisibility: defaultVisibility,
            isDoNotReset: isDoNotReset,
            isRandomFrame: isRandomFrame,
            initialFrame: validatedInitialFrame,
            showFrameCounter: ModelValidationUtils.validateBoolean(model["Show frame counter"]),
            shouldCalcScore: false,
            lang: model["langAttribute"],
            altTexts: validatedAltTexts.altTexts,
            baseDimensions: baseDimensions
        };
    };

    function animateLinearly(element, backgroundPositionValue) {
        $(element).animate({
            'background-position':backgroundPositionValue
        }, 500, "linear");
    }

    function animateLinearlyLastElement(element) {
        $(element).animate({
            'background-position':(100 + presenter.configuration.frameWidthPercentage) + '%'
        }, {
            duration:500,
            easing:"linear",
            complete:function () {
                $(element).css({
                    'background-position':'-' + presenter.configuration.frameWidthPercentage + '%'
                });
                $(element).animate({
                    'background-position':'0%'
                }, 500, "linear")
            }
        });
    }

    function animateLinearlyFirstElement(element) {
        $(element).animate({
            'background-position':'-' + presenter.configuration.frameWidthPercentage + '%'
        }, {
            duration:500,
            easing:"linear",
            complete:function () {
                $(element).css({
                    'background-position':(100 + presenter.configuration.frameWidthPercentage) + '%'
                });
                $(element).animate({
                    'background-position':'100%'
                }, 500, "linear")
            }
        });
    }

    function animateFading(element, backgroundPositionValue) {
        $(element).fadeOut({
            duration: 750,
            complete: function() {
                $(element).css({
                    'background-position': backgroundPositionValue
                });
                $(element).fadeIn(750);
            }
        });
    }

    presenter.changeBackgroundPosition = function (isPreview, element, isReverseOrder) {
        var backgroundPositionValue = (presenter.configuration.currentFrame * presenter.configuration.frameWidthPercentage + '%');

        if (isPreview || presenter.configuration.animation === 'NONE') {
            $(element).css('background-position', backgroundPositionValue);
        } else {
            switch (presenter.configuration.animation) {
                case 'LINEAR':
                    if (isReverseOrder) {
                        if (presenter.configuration.currentFrame === presenter.configuration.frames - 1) {
                            animateLinearlyFirstElement(element);
                        } else {
                            animateLinearly(element, backgroundPositionValue);
                        }
                    } else {
                        if (presenter.configuration.currentFrame !== 0) {
                            animateLinearly(element, backgroundPositionValue);
                        } else {
                            animateLinearlyLastElement(element);
                        }
                    }
                    break;
                case 'FADING':
                    animateFading(element, backgroundPositionValue);
                    break;
            }
        }
    };

    presenter.changeFrameLogic = function (isPreview, triggerEvent) {
        presenter.changeCurrentDot();

        if (presenter.configuration.currentFrame === 0) {
            if (!isPreview) presenter.stopAllAudio();
        } else {
            if (!isPreview) presenter.playAudio();
        }

        if (presenter.configuration.currentVisibility) {
            presenter.displayLabels(presenter.configuration.currentFrame + 1);
        } else {
            presenter.hideLabels();
        }

        if (triggerEvent &amp;&amp; !isPreview) {
            presenter.triggerFrameChangeEvent(presenter.configuration.currentFrame + 1);
        }
    };
    presenter.changeFrame = function(isPreview, isReverseOrder, triggerEvent, readFrame) {
        presenter.changeBackgroundPosition(isPreview, presenter.$element, isReverseOrder);
        presenter.changeFrameLogic(isPreview, triggerEvent);
        if (readFrame) {
            presenter.readFrame(presenter.configuration.currentFrame);
        }
    };

    presenter.stopAllAudio = function () {
        for (var i = 0; i &lt; audioElements.length; i++) {
            if (audioElements[i] !== null ) {
                stopAudio(audioElements[i]);
            }
        }
    };

    function stopAudio(audio) {
        audio.pause();
        audio.setTime(0);
    }

    presenter.playAudio = function() {
        presenter.stopAllAudio();

        var audio = audioElements[presenter.configuration.currentFrame - 1];
        if (audio) {
            audio.play();
        }
    };

    presenter.createEventData = function(frameNumber, eventScore) {
        return {
            source : this.addonId,
            item : "" + frameNumber,
            value : '',
            score : eventScore
        }
    };

    presenter.triggerFrameChangeEvent = function(frameNumber) {
        var eventScore;

        if(presenter.configuration.correctFrames.frames){
            if(presenter.configuration.correctFrames.frames.indexOf(frameNumber-1) &gt; -1){
                eventScore = 1;
            }else{
                eventScore = 0;
            }
        }else{
            eventScore = '';
        }

        var eventData = this.createEventData(frameNumber, eventScore);

        if (playerController != null &amp;&amp; !presenter.isShowAnswersActive) {
            playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
    };

    presenter.isCurrentFrameCorrectlySelected = function () {
        var correctFrames = presenter.configuration.correctFrames.frames;

        for (var i = 0, length = correctFrames.length; i &lt; length; i++) {
            if (presenter.configuration.currentFrame === correctFrames[i]) {
                return true;
            }
        }

        return false;
    };

    presenter.getState = function() {
        return JSON.stringify({
            currentFrame : this.configuration.currentFrame,
            currentVisibility : this.configuration.currentVisibility,
            showWatermark : ($(watermarkElement).is(':visible')) ? true : false,
            showWatermarkbyDefault : this.configuration.showWatermarkbyDefault,
            isClickDisabled: presenter.configuration.isClickDisabled,
            isClickDisabledbyDefault : presenter.configuration.isClickDisabledbyDefault,
            shouldCalcScore: presenter.configuration.shouldCalcScore
        });
    };

    presenter.setState = function(state_string) {
        var configuration = presenter.configuration;
        var state = JSON.parse(state_string);
        configuration.currentFrame = state.currentFrame;
        configuration.currentVisibility = state.currentVisibility;
        configuration.showWatermark = state.showWatermark;
        configuration.showWatermarkbyDefault = state.showWatermarkbyDefault;
        configuration.shouldCalcScore = state.shouldCalcScore;
        configuration.isClickDisabled = state.isClickDisabled;
        configuration.isClickDisabledbyDefault = state.isClickDisabledbyDefault;

        $.when(presenter.imageLoaded).then(loadImageEndCallback);
    };

    presenter.reset = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.shouldCalcScore = true;
        presenter.configuration.isErrorMode = false;

        if (presenter.configuration.isDoNotReset) return;

        presenter.setCurrentFrame();
        presenter.changeFrame(false, false, false, false);

        if (shouldShowWatermark()) {
            showWatermarkIfNotVisible();
        }

        presenter.configuration.isClickDisabled = presenter.configuration.isClickDisabledbyDefault;

        var isVisible = presenter.configuration.currentVisibility = presenter.configuration.defaultVisibility;
        presenter.setVisibility(isVisible);
        removeCorrectnessClasses();
    };

    presenter.getScore = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (!presenter.configuration.correctFrames.isExerciseMode) return 0;
        if (!presenter.configuration.shouldCalcScore) return 0;

        return presenter.isCurrentFrameCorrectlySelected() ? 1 : 0;
    };

    presenter.getErrorCount = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (!presenter.configuration.correctFrames.isExerciseMode) { return 0; }
        if (!presenter.configuration.shouldCalcScore) return 0;

        return presenter.isCurrentFrameCorrectlySelected() ? 0 : 1;
    };

    presenter.getMaxScore = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if(presenter.configuration.correctFrames &amp;&amp; !presenter.configuration.correctFrames.isExerciseMode) { return 0; }
        return 1;
    };

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isErrorMode = true;
        presenter.configuration.shouldCalcScore = true;

        if(!presenter.configuration.correctFrames.isExerciseMode) return;

        var isCorrect = presenter.isCurrentFrameCorrectlySelected();

        presenter.$element.addClass(isCorrect ? 'correct' : 'wrong');
    };

    function removeCorrectnessClasses() {
        presenter.$element.removeClass('correct');
        presenter.$element.removeClass('wrong');
    }

    presenter.setWorkMode = function() {
        presenter.configuration.isErrorMode = false;
        if(!presenter.configuration.correctFrames.isExerciseMode) return;

        removeCorrectnessClasses();
    };

    presenter.onEventReceived = function(eventName) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        }

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.moveToFrameCommandShowAnswers = function(params) {
        var framesCount = presenter.configuration.frames,
            currentFrame = presenter.configuration.currentFrame,
            validatedFrame = ModelValidationUtils.validateIntegerInRange(params[0], framesCount, 1);

        presenter.configuration.shouldCalcScore = true;

        if (validatedFrame.isValid &amp;&amp; validatedFrame.value - 1 !== currentFrame) {
            var isReverseOrder = currentFrame &gt; validatedFrame.value - 1;
            presenter.configuration.currentFrame = validatedFrame.value - 1;

            presenter.changeFrameData = {
                isPreview: false,
                isReverseOrder: isReverseOrder,
                triggerEvent: true
            };

            $.when(presenter.imageLoaded).then(loadImageEndCallback);
        }
    };

    presenter.moveToFrameShowAnswers = function (frame) {
        presenter.moveToFrameCommandShowAnswers([frame]);
    };

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;

        if(presenter.configuration.correctFrames.frames){
            presenter.setWorkMode();
            presenter.currentFrameShowAnswers = presenter.configuration.currentFrame;
            presenter.moveToFrameShowAnswers(presenter.configuration.correctFrames.frames[0]+1);
            presenter.$element.addClass('show-answers');
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        if(presenter.currentFrameShowAnswers != undefined){
            presenter.moveToFrameShowAnswers(presenter.currentFrameShowAnswers+1);
            presenter.$element.removeClass('show-answers');
        }

        presenter.isShowAnswersActive = false;
    };

    presenter.readFrame = function (frameId) {
        if (playerController &amp;&amp; playerController.isWCAGOn()) {
            var voiceObjects = presenter.getTextVoiceObjectForFrame(frameId);
            if (presenter.isWCAGSelected) {
                presenter.speak(voiceObjects);
            } else {
                presenter.speakWhenIdle(voiceObjects);
            }
        }
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {

        if (keycode == window.KeyboardControllerKeys.ENTER) {
            if (!isShiftKeyDown) {
                event.preventDefault();
                presenter.readFrame(presenter.configuration.currentFrame);
            } else {
                presenter.setWCAGStatus(false);
            }

        } else if (keycode == window.KeyboardControllerKeys.SPACE) {
            event.preventDefault();
            if (!presenter.configuration.isClickDisabled) {
                hideWatermarkIfVisible();
                presenter.next();
            }

        } else if (keycode == window.KeyboardControllerKeys.ESCAPE) {
            presenter.isWCAGSelected = false;
        }
    };

    presenter.setWCAGStatus = function(isOn) {
        presenter.isWCAGSelected = isOn;
    };

    presenter.getTextVoiceObjectForFrame = function (frameId) {
        return [window.TTSUtils.getTextVoiceObject(presenter.configuration.altTexts[frameId], presenter.configuration.lang)];
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function(data, callback) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; playerController.isWCAGOn()) {
            tts.speakWithCallback(data, callback);
        }
    };

    presenter.speakWhenIdle = function (data, callback) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; playerController.isWCAGOn()) {
            tts.speakWhenIdle(data, callback);
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Layered_Image" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Base image" nameLabel="Layered_Image_property_base_image" type="image"/>
		<property name="Layers" nameLabel="Layered_Image_property_layers" type="list">
			<property name="Image" nameLabel="Layered_Image_property_image" type="image"/>
			<property name="Show at start" nameLabel="Layered_Image_property_show_at_start" type="boolean"/>
		</property>
		<property name="Image size" nameLabel="Layered_Image_property_image_size" type="{Original, Keep aspect ratio, Stretch}"/>
        <property name="Animated gif refresh" nameLabel="Layered_Image_property_animated_gif_refresh" type="boolean"/>
	</model>
<css>.layeredimage-wrapper,
.layeredimage-image {
    border: 0;
    padding: 0;
    margin: 0;
}

.layeredimage-wrapper {
    position: relative;
}

.layeredimage-image {
    position: absolute;
    background-repeat: no-repeat;
}

.layeredimage-loading {
    display: none;
    z-index: 10;
    position: absolute;
    border: 0 solid black;
    width: 50px;
    height: 50px;
}
</css><view>&lt;div class="layeredimage-wrapper"&gt;
    &lt;div class="layeredimage-image"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="layeredimage-loading"/&gt;
</view><preview>&lt;div class="layeredimage-wrapper"&gt;
    &lt;div class="layeredimage-image"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img src="/media/images/loading.gif" alt="Loading..." class="layeredimage-loading"/&gt;
</preview><presenter>function AddonLayered_Image_create() {
    var presenter = function () {};
    var DOMElements = {};
    presenter.flags = [];
    presenter.savedState = "";
    var elementsDimensions = {};
    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);

    presenter.ERROR_CODES = {
        BI_01: "Base image wasn't set or was set incorrectly!",
        L_01: "At least one layer must be set and no blank layers can be left!"
    };

    presenter.IMAGE_SIZE = {
        ORIGINAL: 0,
        SCALED: 1,
        STRETCHED: 2
    };

    function deferredQueueDecoratorChecker () {
        return presenter.imagesAreLoaded || presenter.diplayingLayers;
    }

    function setDOMElementsHrefAndSelectors(view) {
        DOMElements.$view = $(view);
        DOMElements.wrapper = $(DOMElements.$view.find('.layeredimage-wrapper:first')[0]);
        DOMElements.loading = $(DOMElements.$view.find('.layeredimage-loading:first')[0]);
        DOMElements.baseImage = $(DOMElements.wrapper.find('.layeredimage-image:first')[0]);
    }

    function setElementsDimensions(width, height) {
        var wrapperDimensions = DOMOperationsUtils.getOuterDimensions(DOMElements.wrapper);
        var wrapperDistances = DOMOperationsUtils.calculateOuterDistances(wrapperDimensions);
        var wrapperWidth = width - wrapperDistances.horizontal;
        var wrapperHeight = height - wrapperDistances.vertical;

        $(DOMElements.wrapper).css({
            width: wrapperWidth,
            height: wrapperHeight
        });

        var imageDimensions = DOMOperationsUtils.getOuterDimensions(DOMElements.baseImage);
        var imageDistances = DOMOperationsUtils.calculateOuterDistances(imageDimensions);
        var imageWidth = width - imageDistances.horizontal;
        var imageHeight = height - imageDistances.vertical;

        elementsDimensions = {
            wrapper: {
                width: wrapperWidth,
                height: wrapperHeight
            },
            image: {
                width: imageWidth,
                height: imageHeight
            }
        };
    }

    // Calculate scale for image containing element depending on frame aspect ratio
    function calculateContainerDimensions(imageWidth, imageHeight, containerWidth, containerHeight) {
        var imageRatio = imageWidth / imageHeight;
        var containerRatio = containerWidth / containerHeight;

        var horizontal;
        var vertical;

        if (imageRatio &gt;= containerRatio) {
            horizontal = containerWidth;
            vertical = containerWidth / imageRatio;
        } else {
            vertical = containerHeight;
            horizontal = containerHeight * imageRatio;
        }

        return {
            horizontal: horizontal,
            vertical: vertical
        };
    }

    // This function returns string containing CSS declaration of elements
    // background image size in percentage measure
    function calculateBackgroundSize(size) {
        var cssValue;

        switch (size) {
            case presenter.IMAGE_SIZE.SCALED:
            case presenter.IMAGE_SIZE.STRETCHED:
                cssValue = '100% 100%';
                break;
            default:
                cssValue = '';
        }

        return cssValue;
    }

    function appendImage(image, show) {
        var imageElement = document.createElement('div');
        $(imageElement).css('backgroundImage', 'url(' + image + ')');
        $(imageElement).addClass('layeredimage-image');
        $(DOMElements.wrapper).append(imageElement);

        if (!show) {
            $(imageElement).hide();
        }
    }

    function calculateImageDimensions(image) {
        $(DOMElements.wrapper).append(image);

        $(image).show();
        var imageWidth = $(image).width();
        var imageHeight = $(image).height();
        $(image).hide();

        $(image).remove();
        return { width: imageWidth, height: imageHeight };
    }

    function preloadImages(isPreview) {
        showLoadingScreen();

        var images = [presenter.configuration.baseImage];
        for (var i = 0; i &lt; presenter.configuration.layers.length; i++) {
            images.push(presenter.configuration.layers[i].image);
        }

        $.imgpreload(images, {
            all: function() {
                var imageDimensions = calculateImageDimensions(this[0]);
                loadImages(imageDimensions.width, imageDimensions.height, isPreview);
            }
        });
    }

    function loadImages(imageWidth, imageHeight, isPreview) {
        appendImage(presenter.configuration.baseImage, true);
        DOMElements.baseImage = $(DOMElements.$view.find('.layeredimage-image:first')[0]);

        var containerWidth = elementsDimensions.wrapper.width;
        var containerHeight = elementsDimensions.wrapper.height;
        var containerDimensions = calculateContainerDimensions(imageWidth, imageHeight, containerWidth, containerHeight);
        var backgroundSize = calculateBackgroundSize(presenter.configuration.imageSize);

        var isScaledMode = presenter.configuration.imageSize === presenter.IMAGE_SIZE.SCALED;
        $(DOMElements.baseImage).css({
            width: isScaledMode ? containerDimensions.horizontal + 'px' : containerWidth + 'px',
            height: isScaledMode ? containerDimensions.vertical + 'px' : containerHeight + 'px'
        });
        if (backgroundSize) {
            $(DOMElements.baseImage).css('background-size', backgroundSize);
        }

        for (var i = 0; i &lt; presenter.configuration.layers.length; i++) {
            var showLayer = isPreview ? true : presenter.configuration.layers[i].showAtStart;
            if(!presenter.savedState) {
                setFlag(i, presenter.configuration.layers[i].showAtStart);
            }
            var imageElement = document.createElement('div');
            $(imageElement).css('backgroundImage', 'url(' + presenter.configuration.layers[i].image + ')');
            $(imageElement).addClass('layeredimage-image');
            $(imageElement).attr('data-index', (i+1));
            $(DOMElements.wrapper).append(imageElement);

            if (!showLayer) {
                $(imageElement).hide();
            }

            var layer = DOMElements.wrapper.find('div[data-index="'+ (i+1) +'"]');
            $(layer).css({
                width: isScaledMode ? containerDimensions.horizontal + 'px' : containerWidth + 'px',
                height: isScaledMode ? containerDimensions.vertical + 'px' : containerHeight + 'px'
            });
            if (backgroundSize) {
                $(layer).css('background-size', backgroundSize);
            }
        }

        hideLoadingScreen();

        presenter.imageLoadedDeferred.resolve();

        if(!isPreview){
            if (!presenter.imagesAreLoaded &amp;&amp; !presenter.savedState) {
                executeTasks();
            }
        }
    }

    function executeTasks () {
        presenter.imagesAreLoaded = true;

        deferredSyncQueue.resolve();
    }

    function setFlag(index, value) {
        presenter.flags[index] = value;
    }

    function getFlag(index) {
        return presenter.flags[index];
    }

    function prepareLoadingScreen(containerWidth, containerHeight) {
        $(DOMElements.loading).css({
            top: ((containerHeight - $(DOMElements.loading).height()) / 2) + 'px',
            left: ((containerWidth - $(DOMElements.loading).width()) / 2) + 'px'
        });
    }

    function showLoadingScreen() {
        $(DOMElements.loading).show();
    }

    function hideLoadingScreen() {
        $(DOMElements.loading).hide();
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    function presenterLogic(view, model, isPreview) {
        presenter.imageLoadedDeferred = new jQuery.Deferred();
        presenter.imageLoaded = presenter.imageLoadedDeferred.promise();

        var width = model.Width;
        var height = model.Height;

        setDOMElementsHrefAndSelectors(view);

        if (!isPreview) {
            var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
            if (loadingSrc) $(DOMElements.loading).attr('src', loadingSrc);
        }

        setElementsDimensions(width, height);
        $(DOMElements.baseImage).remove();

        presenter.configuration = presenter.validateModel(model);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(DOMElements.$view, presenter.ERROR_CODES, presenter.configuration.errorCode);
        } else {
            prepareLoadingScreen(width, height);
            preloadImages(isPreview);
            if (!isPreview &amp;&amp; !presenter.configuration.isVisibleByDefault) {
                presenter.hide();
            }
        }
    }

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model) {
        presenter.imagesAreLoaded = false;
        presenterLogic(view, model, false);
    };

    presenter.reset = function(){
        hideAllLayers();
        displayVisibleLayers(true);
        presenter.showLayer(0);

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            flags: presenter.flags
        });
    };

    presenter.setStateCallback = function() {
        displayVisibleLayers(false);

        if (presenter.isVisbleSaved) {
            presenter.show();
        } else {
            presenter.hide();
        }

        if (!presenter.imagesAreLoaded) {
            executeTasks();
        }
    };

    presenter.setState = function(state) {
        this.savedState = JSON.parse(state);

        presenter.isVisbleSaved = this.savedState.isVisible;

        for (var i = 0; i &lt; this.savedState.flags.length; i++) {
            presenter.flags[i] = this.savedState.flags[i];
        }

        $.when(presenter.imageLoaded).then(presenter.setStateCallback);
    };

    presenter.setVisibility = function(isVisible) {
        DOMElements.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'showLayer': presenter.showLayerCommand,
            'hideLayer': presenter.hideLayerCommand,
            'toggleLayer': presenter.toggleLayerCommand,
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    function displayVisibleLayers(displayLayersWithShowAtStart) {
        presenter.diplayingLayers = true;
        for (var i = 0; i &lt; presenter.configuration.layers.length; i++) {
            var layerShouldBeDisplayed = displayLayersWithShowAtStart ? presenter.configuration.layers[i].showAtStart : presenter.flags[i];
            if(layerShouldBeDisplayed) {
                presenter.showLayer(i + 1);
            } else {
                presenter.hideLayer(i + 1);
            }
        }
        presenter.displayingLayers = false;
    }

    presenter.showLayer = deferredSyncQueue.decorate(function(index) {
        if (isNaN(index) || index &lt; 1 || index &gt; presenter.configuration.layers.length) {
            return;
        }

        setFlag(index - 1, true);

        var layer = DOMElements.wrapper.find('div[data-index="'+ index +'"]'),
            $layer = $(layer);

        $layer.show();
        if(presenter.configuration.animatedGifRefresh){
            var backgroundImageUrl = $layer.css('background-image'),
                backgroundImage = backgroundImageUrl.substring(backgroundImageUrl.indexOf('url(') + 'url('.length, backgroundImageUrl.indexOf(')')).replace(/"/g, ""),
                timestamp = new Date().getTime();

            if(backgroundImage.indexOf('?') !== -1) {
                backgroundImage = backgroundImage.substring(0, backgroundImage.indexOf('?'));
            }

            backgroundImage = backgroundImage + '?' + timestamp;
            $layer.css('background-image', '');
            $layer.css('background-image', 'url('+ backgroundImage +')');
        }
    });

    presenter.showLayerCommand = function (params) {
        presenter.showLayer(parseInt(params[0], 10));
    };

    presenter.hideLayer = deferredSyncQueue.decorate(function(index) {
        if (isNaN(index) || index &lt; 1 || index &gt; presenter.configuration.layers.length) {
            return;
        }

        setFlag(index - 1, false);

        var layer = DOMElements.wrapper.find('div[data-index="'+ index +'"]');
        $(layer).hide();
    });

    presenter.hideLayerCommand = function (params) {
        presenter.hideLayer(parseInt(params[0], 10));
    };

    presenter.toggleLayer = deferredSyncQueue.decorate(function(index) {
        if (isNaN(index) || index &lt; 1 || index &gt; presenter.configuration.layers.length) {
            return;
        }

        if (getFlag(index - 1)) {
            this.hideLayer(index);
        } else {
            this.showLayer(index);
        }
    });

    presenter.toggleLayerCommand = function (params) {
        presenter.toggleLayer(parseInt(params[0], 10));
    };

    function hideAllLayers() {
        for (var i = 1; i &lt;= presenter.configuration.layers.length; i++) {
            presenter.hideLayer(i);
        }
    }

    presenter.validateImage = function(image) {
        if (ModelValidationUtils.isStringEmpty(image)) return { isError: true };

        return { isError: false, image: image };
    };

    presenter.validateImageList = function (list) {
        var imageList = [];

        for (var i = 0; i &lt; list.length; i++) {
            var image = list[i].Image;
            if (presenter.validateImage(image).isError) {
                return { isError: true, errorCode: "IL_01" };
            }

            var showAtStart = presenter.showAtStart(list[i]["Show at start"]);
            imageList.push({
                image: image,
                showAtStart: showAtStart
            });
        }

        return { isError: false, list: imageList };
    };

    presenter.validateImageSize = function(imageSize) {
        var result;

        switch (imageSize) {
            case "Keep aspect ratio":
                result = presenter.IMAGE_SIZE.SCALED;
                break;
            case "Stretch":
                result = presenter.IMAGE_SIZE.STRETCHED;
                break;
            default:
                result = presenter.IMAGE_SIZE.ORIGINAL;
                break;
        }

        return result;
    };

    presenter.showAtStart = function(value) {
        return value === "True" || value === "1";
    };

    presenter.validateModel = function (model) {
        var validatedBaseImage = presenter.validateImage(model["Base image"]);
        if (validatedBaseImage.isError) return { isError: true, errorCode: "BI_01" };

        var validatedLayers = presenter.validateImageList(model.Layers);
        if (validatedLayers.isError) return { isError: true, errorCode: "L_01" };

        return {
            isError: false,
            baseImage: validatedBaseImage.image,
            layers: validatedLayers.list,
            imageSize: presenter.validateImageSize(model["Image size"]),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            animatedGifRefresh: ModelValidationUtils.validateBoolean(model["Animated gif refresh"])
        };
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="LearnPen" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="LearnPen" name="SmartPen" nameLabel="LearnPen_property_smart_pen" type="boolean"/>
        <property name="Colors" nameLabel="LearnPen_property_colors" type="text"/>
        <property name="Thickness" nameLabel="LearnPen_property_thickness" type="text"/>
        <property name="Opacity" nameLabel="LearnPen_property_opacity" type="text"/>

        <property name="Squeeze limits" nameLabel="LearnPen_property_squeeze_limits" type="string"/>
        <property name="Squeeze limits interpretation" nameLabel="LearnPen_property_squeeze_limits_interpretation" type="{Separately, Together}"/>
        <property name="Pressure limits" nameLabel="LearnPen_property_pressure_limits" type="string"/>

        <property name="Events" nameLabel="LearnPen_property_events" type="list">
            <property name="Sensor" nameLabel="LearnPen_property_sensor" type="string"/>
            <property name="Reaction scope" nameLabel="LearnPen_property_reaction_scope" type="string"/>
            <property name="Item" nameLabel="LearnPen_property_item" type="string"/>
            <property name="Value" nameLabel="LearnPen_property_value" type="string"/>
            <property name="Score" nameLabel="LearnPen_property_score" type="string"/>
        </property>

        <property name="Mirror" nameLabel="LearnPen_property_mirror" type="boolean"/>
        <property name="Background color" nameLabel="LearnPen_property_background_color" type="string"/>
	</model>
<css>.addon_DrawingSmartPen .drawing .canvas {
    position: absolute;
    left: 0; right: 0;
    bottom: 0; top: 0;

    cursor: crosshair;
}

.addon_DrawingSmartPen .drawing .canvas:active {
    cursor: crosshair;
}

/* https://code.google.com/p/android/issues/detail?id=35474 */
.addon_DrawingSmartPen .drawing {
    overflow: hidden;
    overflow-x: visible;
}
</css><view>&lt;div class="drawing"&gt;&lt;/div&gt;
</view><preview>&lt;div class="drawing"&gt;&lt;/div&gt;
</preview><presenter>function AddonLearnPen_create() {
    var presenter = function() {};

    // work-around for double line in android browser
    function setOverflowWorkAround(turnOn) {

        if (!MobileUtils.isAndroidWebBrowser(window.navigator.userAgent)) { return false; }

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.1.2", "4.2.2", "4.3"].indexOf(android_ver) !== -1) {

            presenter.$view.parents("*").each(function() {
                var overflow = null;
                if (turnOn) {
                    $(this).attr("data-overflow", $(this).css("overflow"));
                    $(this).css("overflow", "visible");
                } else {
                    overflow = $(this).attr("data-overflow");
                    if (overflow !== "") {
                        $(this)[0].removeAttribute("data-overflow");
                        $(this).css("overflow", overflow);
                    }
                }
            });

        }

        return true;
    }

    // constant values
    var val = {
        maxThickness: 40,
        defaultThickness: 15,
        defaultColor: "rgba(0, 0, 0, 0)",
        sensors: ["SQUEEZE_A", "SQUEEZE_B", "SQUEEZE_C", "SQUEEZE_SUM", "SQUEEZE_MAX", "PRESSURE", "ALL"]
    };

    presenter.position = {
        pre_x: 0,
        pre_y: 0,
        x: 0,
        y: 0
    };

    presenter.drawingData = {
        isDrawingOn: true,
        pre_color: val.defaultColor,
        color: val.defaultColor,
        thickness: val.defaultThickness
    };

    presenter.data = {
        canvas: null,
        context: null,

        isStarted: false,
        isPencil: true,
        color: val.defaultColor,
        zoom: 1,
        lineCounter: 0,
        allPoints: [],
        pencilThickness: 0,
        eraserThickness: 0
    };

    var eventBus;

    function updateZoomMultiplier() {
        var zoom = $('#_icplayer').css('zoom');
        if (zoom === '' || zoom === undefined || zoom === "normal") {
            zoom = 1;
        }
        presenter.data.zoom = parseInt(zoom, 10);
    }

    presenter.hexToRGBA = function(hex, opacity) {
        var r = parseInt(hex.substring(1,3), 16),
            g = parseInt(hex.substring(3,5), 16),
            b = parseInt(hex.substring(5,7), 16);

        return 'rgba(' + r + ',' + g + ',' + b + ',' + opacity + ')';
    };

    presenter.colourNameToHex = function(color) {
        var colors = {"aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff","beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887","cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff","darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f","darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1","darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff","firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff","gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f","honeydew":"#f0fff0","hotpink":"#ff69b4","indianred ":"#cd5c5c","indigo ":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c","lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2","lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de","lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6","magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee","mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5","navajowhite":"#ffdead","navy":"#000080","oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6","palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080","red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1","saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4","tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0","violet":"#ee82ee","wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5","yellow":"#ffff00","yellowgreen":"#9acd32"};

        if (typeof colors[color.toLowerCase()] !== 'undefined') {
            return colors[color.toLowerCase()];
        }
        return false;
    };

    function eventCreator(a, b, c, p) {
        function sendSensorEvent(eventData, smartPenValue) {
            if (eventData.reactionScope[0] &lt;= smartPenValue &amp;&amp; smartPenValue &lt;= eventData.reactionScope[1]) {
                eventBus.sendEvent('ItemReturned', {
                    'source': presenter.configuration.id,
                    'item': eventData.item,
                    'value': eventData.value,
                    'score': eventData.score
                });
            }
        }

        var eventsArray = presenter.configuration.events;
        for (var i=0; i&lt;eventsArray.length; i++) {
            switch(eventsArray[i].sensor) {
                case "SQUEEZE_A": sendSensorEvent(eventsArray[i], a); break;
                case "SQUEEZE_B": sendSensorEvent(eventsArray[i], b); break;
                case "SQUEEZE_C": sendSensorEvent(eventsArray[i], c); break;
                case "SQUEEZE_SUM": sendSensorEvent(eventsArray[i], ((a+b+c)/3)); break;
                case "SQUEEZE_MAX": sendSensorEvent(eventsArray[i], Math.max(a,b,c)); break;
                case "PRESSURE": sendSensorEvent(eventsArray[i], p); break;
                case "ALL": sendSensorEvent(eventsArray[i], ((a+b+c+p)/4)); break;
            }
        }
    }

    function areLimitsOk(a, b, c, p) {
        var isPressureOK = presenter.configuration.pressureLimits[0] &lt;= p &amp;&amp; p &lt;= presenter.configuration.pressureLimits[1];

        if (presenter.configuration.squeezeLimitsInterpretation === "TOGETHER") {
            var sum = a + b + c;
            var isSqueezeOK = (presenter.configuration.squeezeLimits[0] * 3) &lt;= sum &amp;&amp; sum &lt;= (presenter.configuration.squeezeLimits[1] * 3);

            return isPressureOK &amp;&amp; isSqueezeOK;
        } else if (presenter.configuration.squeezeLimitsInterpretation === "SEPARATELY") {
            var isSqueezeAOK = presenter.configuration.squeezeLimits[0] &lt;= a &amp;&amp; a &lt;= presenter.configuration.squeezeLimits[1];
            var isSqueezeBOK = presenter.configuration.squeezeLimits[0] &lt;= b &amp;&amp; b &lt;= presenter.configuration.squeezeLimits[1];
            var isSqueezeCOK = presenter.configuration.squeezeLimits[0] &lt;= c &amp;&amp; c &lt;= presenter.configuration.squeezeLimits[1];

            return isSqueezeAOK &amp;&amp; isSqueezeBOK &amp;&amp; isSqueezeCOK &amp;&amp; isPressureOK;
        }

        return false;
    }

    presenter.calculateValue = function(v, collection, defaultVal) {
        var result = defaultVal;
        for (var i=0; i&lt;collection.length; i++) {
            if (collection[i][0] &gt;= v) {
                result = collection[i][1];
                break;
            }
        }
        return result;
    };

    function getValue(a, b, c, p, collection, defaultValue) {
        switch(collection.sensor) {
            case "SQUEEZE_A": return presenter.calculateValue(a, collection.values, defaultValue);
            case "SQUEEZE_B": return presenter.calculateValue(b, collection.values, defaultValue);
            case "SQUEEZE_C": return presenter.calculateValue(c, collection.values, defaultValue);
            case "SQUEEZE_SUM": return presenter.calculateValue(parseInt((a + b + c) / 3, 10), collection.values, defaultValue);
            case "SQUEEZE_MAX": return presenter.calculateValue(Math.max(a, b, c), collection.values, defaultValue);
            case "PRESSURE": return presenter.calculateValue(p, collection.values, defaultValue);
            case "ALL": return presenter.calculateValue(parseInt((a + b + c + p)/ 3, 10), collection.values, defaultValue);
        }
        return false;
    }

    function getColor(a, b, c, p) {
        return getValue(a, b, c, p, presenter.configuration.colors, "#000000");
    }

    function getOpacity(a, b, c, p) {
        return getValue(a, b, c, p, presenter.configuration.opacity, 1);
    }

    function getThickness(a, b, c, p) {
        return getValue(a, b, c, p, presenter.configuration.thickness, val.defaultThickness);
    }

    function updateDrawingData(e) {
        var a, b, c, p;
        if (presenter.configuration.isSmartPen) {
            if (window.LearnPen === undefined) {
                presenter.drawingData.isDrawingOn = true;
                presenter.drawingData.pre_color = presenter.drawingData.color = "black";
                presenter.drawingData.thickness = 1;
            } else {
                a = parseInt(window.LearnPen.getA() / 10, 10);
                b = parseInt(window.LearnPen.getB() / 10, 10);
                c = parseInt(window.LearnPen.getC() / 10, 10);
                p = parseInt(window.LearnPen.getP() / 10, 10);

                eventCreator(a, b, c, p);

                if (areLimitsOk(a, b, c, p)) {
                    presenter.drawingData.isDrawingOn = true;
                    presenter.drawingData.pre_color = presenter.drawingData.color;
                    presenter.drawingData.color = presenter.hexToRGBA(getColor(a, b, c, p), getOpacity(a, b, c, p));
                    presenter.drawingData.thickness = getThickness(a, b, c, p);
                } else {
                    presenter.drawingData.isDrawingOn = false;
                    presenter.noDraw = true;
                }
            }
        } else {
            presenter.drawingData.pre_color = presenter.drawingData.color;
            if (presenter.data.isPencil) {
                presenter.drawingData.color = presenter.hexToRGBA(presenter.data.color, presenter.configuration.opacity);
            }
            presenter.drawingData.thickness = presenter.data.isPencil ? presenter.data.pencilThickness : presenter.data.eraserThickness;
        }

        presenter.position.pre_x = presenter.position.x;
        presenter.position.pre_y = presenter.position.y;

        if (MobileUtils.isEventSupported('touchstart')) {
            presenter.position.x = parseInt(event.changedTouches[0].pageX - $(presenter.data.canvas).offset().left, 10);
            presenter.position.y = parseInt(event.changedTouches[0].pageY - $(presenter.data.canvas).offset().top, 10);
        } else {
            presenter.position.x = parseInt((e.pageX - presenter.data.canvas.offset().left) / presenter.data.zoom, 10);
            presenter.position.y = parseInt((e.pageY - presenter.data.canvas.offset().top) / presenter.data.zoom, 10);
        }
    }

    function resizeCanvas() {
        var con = presenter.$view.find('.drawing').parent();
        presenter.data.canvas[0].width = con.width() - 2;
        presenter.data.canvas[0].height = con.height();
    }

    function fillCanvasWithColor() {
        if (presenter.configuration.backgroundColor !== "NO_BG") {
            presenter.data.context.fillStyle = presenter.configuration.backgroundColor;
            presenter.data.context.fillRect(0, 0, presenter.data.canvas[0].width, presenter.data.canvas[0].height);
        }
    }

    function drawVerticalLineIfIsMirror() {
        if (presenter.configuration.isMirror) {
            var w = presenter.data.canvas[0].width;

            presenter.data.context.beginPath();
            presenter.data.context.moveTo(w / 2, 0);
            presenter.data.context.lineTo(w / 2, presenter.data.canvas[0].height);

            presenter.data.context.lineWidth = presenter.configuration.border;
            presenter.data.context.strokeStyle = 'black';
            presenter.data.context.stroke();
        }
    }

    function createCanvas(isPreview) {
        presenter.data.isPencil = true;
        presenter.data.pencilThickness = presenter.configuration.thickness;

        presenter.$view.find('.drawing').append("&lt;canvas class='canv'&gt;Canvas is not supported by your browser&lt;/canvas&gt;");

        presenter.data.canvas = presenter.$view.find('canvas');
        presenter.data.context = presenter.data.canvas[0].getContext("2d");

        presenter.$view.find('.drawing').css('opacity', presenter.configuration.opacity);
        resizeCanvas();

        fillCanvasWithColor();
        drawVerticalLineIfIsMirror();

        if (!isPreview) {
            presenter.turnOnEventListeners();
        }

        presenter.setVisibility(presenter.configuration.isVisible);
    }

    function returnErrorObject(errorCode) {
        return { isValid: false, errorCode: errorCode };
    }

    function returnCorrectObject(v) {
        return { isValid: true, value: v };
    }

    presenter.SQ_LIMITS_INTERPRETATION = {
        'Separately': 'SEPARATELY',
        'Together': 'TOGETHER',
        DEFAULT: 'Separately'
    };

    presenter.ERROR_CODES = {
        F01: "Value range has to be between 0 and 100%",
        F02: "Range values has to be ascending",

        S01: "Value sensor cannot be empty",
        S02: "Wrong sensor name",

        C01: "Property colors cannot be empty",
        C02: "One of the value in property colors has wrong length in hex format, should be # and 6 digits [0 - F]",
        C03: "One of the value in property color has wrong color name",

        T01: "Property thickness cannot be empty when SmartPen is on",
        T02: "Property thickness cannot be smaller than 1 and bigger then " + val.maxThickness,

        O02: "Property opacity cannot be smaller than 0 and bigger than 1",

        SQL01: "Wrong amount of values in property squeeze limits",
        SQL02: "Values in property squeeze limits must be numeric",
        SQL03: "2nd value in property squeeze limits cannot be smaller than 1st",
        SQL04: "Values in property squeeze limits cannot be smaller than 0",
        SQL05: "Values in property squeeze limits cannot be bigger than 100",

        PL01: "Wrong amount of numbers in property pressure limits",
        PL02: "Values in Property pressure limits must be numeric",
        PL03: "2nd value in property pressure limits cannot be smaller than 1st",
        PL04: "Values in property pressure limits cannot be smaller than 0",
        PL05: "Values in property pressure limits cannot be bigger than 100",

        E01: "Value in property Events cannot be different then 0 or 1",
        E02: "Score in property Events cannot be different then 0 or 1",

        RS02: "Wrong amount of numbers in Reaction scope in Events property",
        RS03: "Values in Reaction scope in Events property must be numeric",
        RS04: "2nd value in Reaction scope in Events property cannot be smaller than 1st",
        RS05: "Values in Reaction scope in Events property cannot be smaller than 0",
        RS06: "Values in Reaction scope in Events property cannot be bigger than 100",

        B02: "Property border cannot be smaller than 0",
        B03: "Property border cannot be bigger than 5"
    };

    function addPointsToHistory(x, y) {
        presenter.data.allPoints.push({x: x, y: y});
    }

    function sendEvent() {
        function round(num, x) {
            x = Math.pow(10, x);
            return Math.round(num * x) / x;
        }

        var distance = 0;

        for (var i = 0; i &lt; presenter.data.allPoints.length-1; i++) {
            distance += Math.sqrt(Math.pow(presenter.data.allPoints[i].x - presenter.data.allPoints[i+1].x, 2) + Math.pow(presenter.data.allPoints[i].y - presenter.data.allPoints[i+1].y, 2));
        }

        if (presenter.data.allPoints.length !== 0) {
            eventBus.sendEvent('ItemReturned', {
                'source': presenter.configuration.id,
                'item': presenter.data.lineCounter++,
                'value': round(distance, 2),
                'score': presenter.mouseleave || presenter.noDraw ? '0' : '1'
            });
        }

        presenter.data.allPoints = [];
    }

    presenter.turnOnEventListeners = function() {
        var canvas = presenter.data.canvas,
            ctx = presenter.data.context,
            isDown = false;

        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';

        // TOUCH events
        if (MobileUtils.isEventSupported('touchstart')) {
            canvas.on('touchend', function() {
                canvas.off('touchmove', presenter.onPainting);
                sendEvent();
                setOverflowWorkAround(false);
            });

            canvas.on('touchstart', function(e) {
                presenter.data.isStarted = true;
                presenter.noDraw = false;

                updateZoomMultiplier();
                presenter.drawPoint();
                canvas.on('touchmove', presenter.onPainting);
                setOverflowWorkAround(true);
            });
        } else { // MOUSE events
            canvas.on('mouseup', function() {
                canvas.off('mousemove', presenter.onPainting);

                sendEvent();
                isDown = false;
                presenter.mouseleave = false;
                setOverflowWorkAround(false);
            });

            canvas.on('mouseleave', function() {
                canvas.off('mousemove', presenter.onPainting);

                if(isDown) {
                    sendEvent();
                    isDown = false;
                    presenter.mouseleave = true;
                }
                setOverflowWorkAround(false);
            });

            canvas.on('mousedown', function(e) {
                //updateDrawingData(e);
                presenter.data.isStarted = true;
                presenter.noDraw = false;
                isDown = true;

                updateZoomMultiplier();
                presenter.drawPoint(e);

                canvas.on('mousemove', presenter.onPainting);
                setOverflowWorkAround(true);
            });
        }

        function createGradient(pre_x, pre_y, x, y) {
            var grad = presenter.data.context.createLinearGradient(pre_x, pre_y, x, y);

            grad.addColorStop(0, presenter.data.isPencil ? presenter.drawingData.pre_color : "rgba(0,0,0,1)");
            grad.addColorStop(1, presenter.data.isPencil ? presenter.drawingData.color : "rgba(0,0,0,1)");

            return grad;
        }

        presenter.onPainting = function(e) {
            e.preventDefault();
            e.stopPropagation();
            updateDrawingData(e);

            if (presenter.drawingData.isDrawingOn) {
                updateZoomMultiplier();
                addPointsToHistory(presenter.position.x, presenter.position.y);
                ctx.lineWidth = presenter.drawingData.thickness;

                ctx.beginPath();
                ctx.moveTo(presenter.position.pre_x, presenter.position.pre_y);
                ctx.lineTo(presenter.position.x, presenter.position.y);
                ctx.strokeStyle = createGradient(presenter.position.pre_x, presenter.position.pre_y, presenter.position.x, presenter.position.y);
                ctx.stroke();

                if (presenter.configuration.isMirror) {
                    var w = presenter.data.canvas[0].width - presenter.position.x;
                    var pre_w = presenter.data.canvas[0].width - presenter.position.pre_x;

                    ctx.beginPath();
                    ctx.moveTo(pre_w, presenter.position.pre_y);
                    ctx.lineTo(w, presenter.position.y);
                    ctx.strokeStyle = createGradient(pre_w, presenter.position.pre_y, w, presenter.position.y);
                    ctx.stroke();
                }
            }
        };

        presenter.drawPoint = function(e) {
            updateDrawingData(e);

            if (presenter.drawingData.isDrawingOn) {
                ctx.lineWidth = presenter.drawingData.thickness;

                ctx.beginPath();
                ctx.moveTo(presenter.position.x, presenter.position.y);
                ctx.lineTo(presenter.position.x+1, presenter.position.y);
                ctx.strokeStyle = createGradient(presenter.position.x, presenter.position.y, presenter.position.x+1, presenter.position.y);
                ctx.stroke();

                if (presenter.configuration.isMirror) {
                    var w = presenter.data.canvas[0].width - presenter.position.x;

                    ctx.beginPath();
                    ctx.moveTo(w, presenter.position.y);
                    ctx.lineTo(w, presenter.position.y+1);
                    ctx.strokeStyle = createGradient(w+1, presenter.position.y, w, presenter.position.y);
                    ctx.stroke();
                }
            }
        };

        canvas.on("click", function() {
            event.stopPropagation();
        });
    };

    function prepareFunctionsValues(fun) {
        fun = Helpers.splitLines(fun);
        var sensor = fun[0].toUpperCase();
        var parsedValues = [];

        if (val.sensors.indexOf(sensor) === -1) {
            return returnErrorObject("S02");
        }

        var values = fun.slice(1);

        var previousNumber = 0;
        for (var i=0; i&lt;values.length; i++) {
            var tmp = values[i].split("%");
            var number = parseInt(tmp[0].trim(), 10);

            if (number &lt; 0 || number &gt; 100) {
                return returnErrorObject("F01");
            }

            if (previousNumber &gt;= number) {
                return returnErrorObject("F02")
            }

            parsedValues.push([number, tmp[1].trim()]);
            previousNumber = number;
        }

        return returnCorrectObject({
            sensor: sensor,
            values: parsedValues
        });
    }

    presenter.parseColors = function(colors, isLearnPen) {
        function checkColor(colors) {
            if (colors[0] === '#' &amp;&amp; !(colors.length === 7 || colors.length === 4)) {
                return returnErrorObject("C02");
            }

            if (colors[0] !== '#') {
                colors = presenter.colourNameToHex(colors);

                if (!colors) {
                    return returnErrorObject("C03");
                }
            }

            return returnCorrectObject(colors);
        }

        if (ModelValidationUtils.isStringEmpty(colors)) {
            return returnErrorObject("C01");
        }

        if (isLearnPen) {

            var parsedFunction = prepareFunctionsValues(colors);
            if (!parsedFunction.isValid) {
                return returnErrorObject(parsedFunction.errorCode);
            }

            for (var i=0; i&lt;parsedFunction.value.values.length; i++) {
                var localColor = parsedFunction.value.values[i][1];
                var checked = checkColor(localColor);
                if (!checked.isValid) {
                    return returnErrorObject(checked.errorCode);
                }
                parsedFunction.value.values[i][1] = checked.value;
            }

            return returnCorrectObject({
                sensor: parsedFunction.value.sensor,
                values: parsedFunction.value.values
            });
        } else {
            return checkColor(colors);
        }
    };

    presenter.parseThickness = function(thickness, isSmartPen) {
        if (ModelValidationUtils.isStringEmpty(thickness)) {
            return isSmartPen ? returnErrorObject("T01") : returnCorrectObject(val.defaultThickness);
        }

        if (isSmartPen) {
            var parsedFunction = prepareFunctionsValues(thickness);
            if (!parsedFunction.isValid) {
                return returnErrorObject(parsedFunction.errorCode);
            }

            for (var i=0; i&lt;parsedFunction.value.values.length; i++) {
                var localThickness = parsedFunction.value.values[i][1];
                if (1 &gt; localThickness || localThickness &gt; val.maxThickness) {
                    return returnErrorObject("T02");
                }
            }

            return returnCorrectObject({
                sensor: parsedFunction.value.sensor,
                values: parsedFunction.value.values //.map(function(a) { return [a[0], parseInt(a[1], 10)]; }) // parse 2nd value to integer
            });
        } else {
            thickness = parseInt(thickness, 10);
            if (1 &gt; thickness || thickness &gt; val.maxThickness) {
                return returnErrorObject("T02");
            }
            return returnCorrectObject(thickness);
        }
    };

    presenter.parseOpacity = function(opacity, isSmartPen) {
        if (ModelValidationUtils.isStringEmpty(opacity)) {
            return returnCorrectObject(isSmartPen ? { sensor: "ALL", values: [[100, 1]] } : 1);
        }

        if (isSmartPen) {
            var parsedFunction = prepareFunctionsValues(opacity);
            if (!parsedFunction.isValid) {
                return returnErrorObject(parsedFunction.errorCode);
            }

            for (var i=0; i&lt;parsedFunction.value.values.length; i++) {
                var localOpacity = parsedFunction.value.values[i][1];
                if (0 &gt; localOpacity || localOpacity &gt; 1) {
                    return returnErrorObject("O02");
                }
            }

            return returnCorrectObject({
                sensor: parsedFunction.value.sensor,
                values: parsedFunction.value.values.map(function(a) { return [a[0], parseFloat(a[1])]; }) // parse 2nd value to float
            });
        } else {
            if (0 &gt; opacity || opacity &gt; 1) {
                return returnErrorObject("O02");
            }

            return returnCorrectObject(opacity);
        }
    };

    function parseRange(range, code) {
        function isNumber(n) {
            return !isNaN(parseFloat(n)) &amp;&amp; isFinite(n);
        }

        if (ModelValidationUtils.isStringEmpty(range)) {
            return returnCorrectObject([0, 100]);
        }

        range = range.split(';').map($.trim).map(parseFloat);
        range = range.length === 1 ? [0].concat(range) : range;

        if (range.length !== 2) {
            return returnErrorObject(code + '01');
        }

        if (!isNumber(range[0]) || !isNumber(range[1])) {
            return returnErrorObject(code + '02');
        }

        if (range[0] &gt;= range[1]) {
            return returnErrorObject(code + '03');
        }

        if (range[0] &lt; 0 || range[1] &lt; 0) {
            return returnErrorObject(code + '04');
        }

        if (range[0] &gt; 100 || range[1] &gt; 100) {
            return returnErrorObject(code + '05');
        }

        return returnCorrectObject(range);
    }

    presenter.parseSqueeze = function(crush) {
        return parseRange(crush, 'SQL');
    };

    presenter.parsePressure = function(pressure) {
        return parseRange(pressure, 'PL');
    };

    presenter.parseEvents = function(events) {
        function isEventEmpty(eventData) {
            return ModelValidationUtils.isStringEmpty(eventData["Sensor"]) &amp;&amp;
                ModelValidationUtils.isStringEmpty(eventData["Reaction scope"]) &amp;&amp;
                ModelValidationUtils.isStringEmpty(eventData["Item"]) &amp;&amp;
                ModelValidationUtils.isStringEmpty(eventData["Value"]) &amp;&amp;
                ModelValidationUtils.isStringEmpty(eventData["Score"]);
        }

        if (isEventEmpty(events[0])) {
            return returnCorrectObject([]);
        }

        var eventsArray = [];

        for (var i=0; i&lt;events.length; i++) {
            var sensor = events[i]["Sensor"];
            if (ModelValidationUtils.isStringEmpty(sensor)) {
                return returnErrorObject("S01");
            }
            if (val.sensors.indexOf(sensor) === -1) {
                return returnErrorObject("S02");
            }

            var reactionScope = events[i]["Reaction scope"];
            var parsedReactionScope = parseRange(reactionScope, "RS");
            if (!parsedReactionScope.isValid) {
                return returnErrorObject(parsedReactionScope.errorCode);
            }

            var value = events[i]["Value"];
            if (value !== '1' &amp;&amp; value !== '0') {
                return returnErrorObject("E01");
            }

            var score = events[i]["Score"];
            if (score !== '1' &amp;&amp; score !== '0') {
                return returnErrorObject("E02");
            }

            eventsArray.push({
                sensor: sensor,
                reactionScope: parsedReactionScope.value,
                item: events[i]["Item"],
                value: value,
                score: score
            });
        }

        return returnCorrectObject(eventsArray);
    };

    presenter.parseBGColor = function(color) {
        if (ModelValidationUtils.isStringEmpty(color)) {
            return returnCorrectObject("NO_BG");
        }

        if (color[0] === '#' &amp;&amp; !(color.length === 7)) {
            return returnErrorObject('C02');
        }

        if (color[0] !== '#') {
            color = presenter.colourNameToHex(color);

            if (!color) {
                return returnErrorObject('C03');
            }
        }

        return returnCorrectObject(color);
    };

    presenter.parseBorder = function(border) {

        if (ModelValidationUtils.isStringEmpty(border)) {
            return returnCorrectObject(0);
        }

        if (border &lt; 0) {
            return returnErrorObject('B02');
        }

        if (border &gt; 5) {
            return returnErrorObject('B03');
        }

        return returnCorrectObject(border);
    };

    presenter.validateModel = function(model) {
        var isSmartPen = ModelValidationUtils.validateBoolean(model["SmartPen"]);

        var parsedColors = presenter.parseColors(model["Colors"], isSmartPen);
        if (!parsedColors.isValid) {
            return returnErrorObject(parsedColors.errorCode);
        }

        var parsedThickness = presenter.parseThickness(model["Thickness"], isSmartPen);
        if (!parsedThickness.isValid) {
            return returnErrorObject(parsedThickness.errorCode);
        }

        var parsedOpacity = presenter.parseOpacity(model["Opacity"], isSmartPen);
        if (!parsedOpacity.isValid) {
            return returnErrorObject(parsedOpacity.errorCode);
        }

        var parsedSqueeze = presenter.parseSqueeze(model["Squeeze limits"]);
        if (!parsedSqueeze.isValid) {
            return returnErrorObject(parsedSqueeze.errorCode);
        }

        var parsedPressure = presenter.parsePressure(model["Pressure limits"]);
        if (!parsedPressure.isValid) {
            return returnErrorObject(parsedPressure.errorCode);
        }

        var parsedEvent = presenter.parseEvents(model["Events"]);
        if (!parsedEvent.isValid) {
            return returnErrorObject(parsedEvent.errorCode);
        }

        var parsedBGColor = presenter.parseBGColor(model["Background color"]);
        if (!parsedBGColor.isValid) {
            return returnErrorObject(parsedBGColor.errorCode);
        }

        return {
            isSmartPen: isSmartPen,
            colors: parsedColors.value, // if (isSmartPen) {sensor: String, values: [Integer, Hex color] } else String (hex color)
            thickness: parsedThickness.value, // if (isSmartPen) {sensor: String, thicknesses: [Integer, Integer] } else Integer
            opacity: parsedOpacity.value, // if (isSmartPen) {sensor: String, opacity: [Integer, Float] } else Float
            squeezeLimits: parsedSqueeze.value,
            squeezeLimitsInterpretation: ModelValidationUtils.validateOption(presenter.SQ_LIMITS_INTERPRETATION, model["Squeeze limits interpretation"]),
            pressureLimits: parsedPressure.value,
            events: parsedEvent.value,
            isMirror: ModelValidationUtils.validateBoolean(model["Mirror"]),
            backgroundColor: parsedBGColor.value,

            width: model["Width"],
            height: model["Height"],
            id: model["ID"],
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"])
        };
    };

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        if (!presenter.configuration.isSmartPen) presenter.data.color = presenter.configuration.colors;
        presenter.data.pencilThickness = presenter.configuration.thickness;
        presenter.data.eraserThickness = presenter.configuration.thickness;

        createCanvas(isPreview);

        return false;
    };

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.executeCommand = function(name, params) {
        if (!presenter.configuration.isValid) {
            return false;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);

        return false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

//    presenter.setShowErrorsMode = function() {};
//    presenter.setWorkMode = function() {};

    presenter.setPlayerController = function(controller) {
        eventBus = controller.getEventBus();
    };

    presenter.reset = function() {
        presenter.$view.find('.canv').remove();
        createCanvas(false);

        presenter.data.isStarted = false;
        presenter.data.lineCounter = 0;
    };

//     presenter.getErrorCount = function() {};
//     presenter.getMaxScore = function() {};
//     presenter.getScore = function() {};

    presenter.getState = function() {
        if (!presenter.data.isStarted) {
            return;
        }

        var c = presenter.$view.find("canvas")[0];

        return JSON.stringify({
            isPencil: presenter.data.isPencil,
            color: presenter.data.color,
            pencilThickness: presenter.data.pencilThickness,
            eraserThickness: presenter.data.eraserThickness,
            data: c.toDataURL("image/png"),
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var savedImg = new Image();

        savedImg.onload = function() {
            presenter.data.context.drawImage(savedImg, 0, 0);

            presenter.data.pencilThickness = JSON.parse(state).pencilThickness;
            presenter.data.eraserThickness = JSON.parse(state).eraserThickness;
            presenter.configuration.isVisible = JSON.parse(state).isVisible;
            presenter.data.isPencil = JSON.parse(state).isPencil;
            presenter.data.isStarted = true;

            if (!presenter.configuration.isSmartPen) {
                if (presenter.data.isPencil) {
                    presenter.setColor(JSON.parse(state).color);
                } else {
                    presenter.setEraserOn();
                }
            }
        };
        savedImg.src = JSON.parse(state).data;
    };

    presenter.setColor = function(color) {
        if (!presenter.configuration.isSmartPen) {
            presenter.data.isPencil = true;
            presenter.data.context.globalCompositeOperation = "source-over";
            presenter.data.color = presenter.parseColors(color, false).value;
        }
    };

    presenter.setThickness = function(thickness) {
        if (!presenter.configuration.isSmartPen) {
            if (presenter.data.isPencil) {
                presenter.data.pencilThickness = thickness;
            } else {
                presenter.data.eraserThickness = thickness;
            }
        }
    };

    presenter.setEraserOn = function() {
        if (!presenter.configuration.isSmartPen) {
            presenter.data.isPencil = false;
            presenter.data.context.globalCompositeOperation = "destination-out";
            presenter.configuration.color = "rgba(255, 255, 255, 1)";
        }
    };

    presenter.isLearnPenConnected = function() {
        return window.LearnPen !== undefined
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="LearnPen_Data" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="isDisable" nameLabel="LearnPen_Data_property_is_disable" type="boolean"/>
        <property name="stepsAndColors" nameLabel="LearnPen_Data_property_colors_and_steps" type="text"/>
        <property name="refreshTime" nameLabel="LearnPen_Data_property_refresh_time" type="string"/>
    </model>
<css>.row {
    height: 25%;
    width: 100%;
}

.sensor-a, .sensor-b, .sensor-c {
    margin-bottom: 5%;
    height: 15%;
}

.sensor-p {
    height: 20%;
}

.box {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;

    float: left;

    border: 1px solid white;

    width: 8.3%;
    height: 100%;
}

.on-mode {
    opacity: 1;
}
</css><view>&lt;div class="sensor-a"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-b"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-c"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-p"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="sensor-a"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-b"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-c"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sensor-p"&gt;
    &lt;div data-step="0" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="1" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="2" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="3" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="4" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="5" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="6" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="7" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="8" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="9" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="10" class="box"&gt;&lt;/div&gt;
    &lt;div data-step="11" class="box"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLearnPen_Data_create() {

    function getErrorObject(ec) { return { isValid: false, errorCode: ec }; }
    function getCorrectObject(val) { return { isValid: true, value: val }; }
    function getStep(col, val) { return { color: col, value: val }; }

    Number.prototype.half = Number.prototype.half || function() {
        return Math.round(this / 2);
    };

    var DATA_LENGTH = 12;
    var ON_MODE_CLASS = 'on-mode';

    var isIntervalOn = false;

    var presenter = function() {};

    function recording(on) {
        if (on) {
            if (!isIntervalOn) {
                presenter.intervalId = setInterval(updateDataOnGraph, presenter.configuration.refreshTime);
            }
        } else {
            if (isIntervalOn) {
                clearInterval(presenter.intervalId);
            }
        }
        isIntervalOn = on;
    }

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    function getCurrentDataFromSensor() {
        function toPercent(val) { return Math.round(val / 1024 * 100); }
        var learnPenData = window.LearnPen;

        return {
            a: learnPenData ? toPercent(learnPenData.getA()) : 0, // Math.round(Math.floor(Math.random() * 100)),
            b: learnPenData ? toPercent(learnPenData.getB()) : 0, // Math.round(Math.floor(Math.random() * 100)),
            c: learnPenData ? toPercent(learnPenData.getC()) : 0, // Math.round(Math.floor(Math.random() * 100)),
            p: learnPenData ? toPercent(learnPenData.getP()) : 0 // Math.round(Math.floor(Math.random() * 100))
        };
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    function colorNameToHex(color) {
        var colors = {
            "aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff","beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887","cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff","darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f","darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1","darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff","firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff","gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f","honeydew":"#f0fff0","hotpink":"#ff69b4","indianred ":"#cd5c5c","indigo ":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c","lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2","grey":"#808080","lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de","lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6","magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee","mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5","navajowhite":"#ffdead","navy":"#000080","oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6","palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080","red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1","saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4","tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0","violet":"#ee82ee","wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5","yellow":"#ffff00","yellowgreen":"#9acd32"
        };

        if (typeof colors[color.toLowerCase()] !== 'undefined') {
            return colors[color.toLowerCase()];
        }

        return color;
    }

    presenter.ERROR_CODES = {
        S01: 'Wrong values in Steps and colors property',
        S02: 'Wrong number of values in Steps and colors property. 12 values for every sensor',
        S03: 'Percentage values in Steps and colors property have to be in descend order',

        T01: 'Property Refresh time cannot be lower then 50 and higher then 2000',
        T02: 'Property Refresh time has to be numeric'
    };

    presenter.isSensorLine = function(line) {
        return line.split(';').every(function(v) {
            return /^[A-CP]$/.test(v.trim());
        });
    };

    presenter.isValueLine = function(line) {
        // 50%;red | 90% ; blue | 3%;#111111 | 77%;#333
        return /^[0-9]{1,3}%\s*;\s*.*([a-zA-Z]|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3})$/.test(line);
    };

    function validateLines(lines) {
        if (lines.length % (DATA_LENGTH + 1) !== 0) return getErrorObject('S02');

        var previous = 0;
        for (var i=0; i&lt;lines.length; i++) {
            if (i % (DATA_LENGTH + 1) === 0) {
                previous = 0;
                if (!presenter.isSensorLine(lines[i])) {
                    return getErrorObject('S01');
                }
            } else {
                if (!presenter.isValueLine(lines[i])) {
                    return getErrorObject('S02');
                }

                var currentPercent = parseInt(lines[i].split(';')[0].trim(), 10);
                if (previous &gt; currentPercent) {
                    return getErrorObject('S03');
                }
                previous = currentPercent;
            }
        }

        return getCorrectObject(null);
    }

    function getDataFromLines(lines, sensor) {
        var sensorSteps = [];

        function findSensorInLine() {
            for (var i=0; i&lt;lines.length; i++) {
                var values = lines[i].split(';');

                if (values.indexOf(sensor) !== -1) {
                    return i;
                }
            }
            return -1;
        }

        var firstValuePosition = findSensorInLine() + 1;

        for (var i=firstValuePosition; i&lt;firstValuePosition+DATA_LENGTH; i++) {
            var stepAndColor = lines[i].split(';').map(function(a) { return a.trim(); });
            var value = parseInt(stepAndColor[0], 10); // removes implicitly % sign
            var color = colorNameToHex(stepAndColor[1]);
            sensorSteps.push(getStep(color, value));
        }

        return sensorSteps;
    }

    function validateColorsAndSteps(data) {
        if (ModelValidationUtils.isStringEmpty(data)) {
            // #ff0000 - red
            // #ffff00 - yellow
            // #008000 - green
            // #ffa500 - orange
            // #90ee90 - lightgreen
            var squeeze = [
                getStep("#ffff00", 10), getStep("#ffff00", 20), getStep("#ffff00", 30),
                getStep("#ffff00", 32),
                getStep("#008000", 33), getStep("#008000", 40), getStep("#008000", 50),
                getStep("#008000", 66),
                getStep("#ff0000", 67),
                getStep("#ff0000", 70), getStep("#ff0000", 80), getStep("#ff0000", 95)
            ];

            return getCorrectObject({
                a: squeeze,
                b: squeeze,
                c: squeeze,
                p: squeeze
            });
        }

        var lines = Helpers.splitLines(data).map(function(line) { return line.trim(); }).filter(function(line) { return line !== '' });

        var validationData = validateLines(lines);
        if (!validationData.isValid) {
            return getErrorObject(validationData.errorCode);
        }

        return getCorrectObject({
            a: getDataFromLines(lines, 'A'),
            b: getDataFromLines(lines, 'B'),
            c: getDataFromLines(lines, 'C'),
            p: getDataFromLines(lines, 'P')
        });
    }

    function validateTime(time) {
        if (ModelValidationUtils.isStringEmpty(time)) {
            return getCorrectObject(100);
        }

        if (!/^[0-9]+$/.test(time)) {
            return getErrorObject('T02');
        }

        time = parseInt(time, 10);

        if (time &lt; 50 || time &gt; 2000) {
            return getErrorObject('T01');
        }

        return getCorrectObject(time);
    }

    presenter.validateModel = function(model) {
        var validatedStepsAndColors = validateColorsAndSteps(model.stepsAndColors);
        if (!validatedStepsAndColors.isValid) return getErrorObject(validatedStepsAndColors.errorCode);

        var validatedTime = validateTime(model.refreshTime);
        if (!validatedTime.isValid) return getErrorObject(validatedTime.errorCode);

        return {
            isValid: true,
            isDisable: ModelValidationUtils.validateBoolean(model.isDisable),
            stepsAndColors: validatedStepsAndColors.value,
            refreshTime: validatedTime.value,
            width: parseInt(model.Width, 10),
            height: parseInt(model.Height, 10),

            id: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"])
        };
    };

    function colorSteps(stepsAndColors) {
        function setBackGroundColor($sensor, step, color) {
            $sensor.find('div.box[data-step="' + step + '"]').css('background-color', color);
            $sensor.find('div.box[data-step="' + step + '"]').attr('data-color', color);
        }

        for (var i=0; i&lt;DATA_LENGTH; i++) {
            setBackGroundColor(presenter.$a, i, stepsAndColors.a[i].color);
            setBackGroundColor(presenter.$b, i, stepsAndColors.b[i].color);
            setBackGroundColor(presenter.$c, i, stepsAndColors.c[i].color);
            setBackGroundColor(presenter.$p, i, stepsAndColors.p[i].color);
        }
    }

    function updateDataOnGraph() {
        function setDataForSensor($sensor, steps, value) {
            var color, index;

            for (var i=0; i&lt;DATA_LENGTH; i++) {
                if (steps[i].value &lt; value) {
                    index = i;
                }
            }
            color = $sensor.find('div.box[data-step="' + index + '"]').attr("data-color");

            for (var i=0; i&lt;DATA_LENGTH; i++) {
                if (steps[i].value &lt; value) {
                    $sensor.find('div.box[data-step="' + i + '"]').css("background-color", color);
                } else {
                    $sensor.find('div.box[data-step="' + i + '"]').css("background-color", "white");
                }
            }
        }

        presenter.$view.find('div.' + ON_MODE_CLASS).each(function() {
            $(this).removeClass(ON_MODE_CLASS);
        });

        var data = getCurrentDataFromSensor();

        setDataForSensor(presenter.$a, presenter.configuration.stepsAndColors.a, data.a);
        setDataForSensor(presenter.$b, presenter.configuration.stepsAndColors.b, data.b);
        setDataForSensor(presenter.$c, presenter.configuration.stepsAndColors.c, data.c);
        setDataForSensor(presenter.$p, presenter.configuration.stepsAndColors.p, data.p);
    }

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.$view = $(view);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        presenter.$a = presenter.$view.find('div.sensor-a');
        presenter.$b = presenter.$view.find('div.sensor-b');
        presenter.$c = presenter.$view.find('div.sensor-c');
        presenter.$p = presenter.$view.find('div.sensor-p');

        colorSteps(presenter.configuration.stepsAndColors);

        if (!isPreview &amp;&amp; !presenter.configuration.isDisable) {
            presenter.intervalId = setInterval(updateDataOnGraph, presenter.configuration.refreshTime);
            isIntervalOn = true;
        }

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        return true;
    };

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.setShowErrorsMode = function() {
        recording(false);
    };

    presenter.setWorkMode = function() {
        recording(true);
    };

    presenter.getState = function() {
        recording(false);
    };

    presenter.setState = function(_) {
        recording(true);
    };

//    presenter.reset = function() { };
//    presenter.getErrorCount = function() { return 0; };
//    presenter.getMaxScore = function() { return 0; };
//    presenter.getScore = function() { return 0; };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="LearnPen_Report" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="isDisable" nameLabel="LearnPen_Report_property_is_disable" type="boolean"/>
        <property name="correctRange" nameLabel="LearnPen_Report_property_correct_range" type="string"/>
        <property name="graphType" nameLabel="LearnPen_Report_property_graph_type" type="{Pie chart, Circle in circle, Four circles, Horizontal Bar}"/>
        <property name="sensor" nameLabel="LearnPen_Report_property_sensor" type="{All, Squeeze, Pressure, Squeeze A, Squeeze B, Squeeze C}"/>
        <property name="colors" nameLabel="LearnPen_Report_property_colors" type="text"/>
        <property name="dataUpdateInterval" nameLabel="LearnPen_Report_property_update_interval" type="string"/>
        <property name="calculateFromLastValues" nameLabel="LearnPen_Report_property_get_last_values" type="string"/>
    </model>
<css/><view>&lt;canvas style="display: none;"&gt;&lt;/canvas&gt;
&lt;img src=""&gt;
</view><preview>&lt;canvas&gt;Canvas is not supported in your browser&lt;/canvas&gt;
</preview><presenter>function AddonLearnPen_Report_create() {

    // utilities functions
    function getCorrectObject(val) { return { isValid: true, value: val } }

    function getErrorObject(ec) { return { isValid: false, errorCode: ec } }

    function isInteger(n) { return n % 1 === 0; }

    function round(n, precision) { return Math.round(n * Math.pow(10, precision)) / Math.pow(10, precision); }

    function getLastElements (array, num) {
        num = num || 1;

        if (array.length &lt; num) {
            return this;
        }

        return this.slice(array.length - num);
    }

    function shouldGetDataFromSensors (){
        for (var addon in presenter.addons) {
            if(presenter.addons[addon] == true){
                return true;
            }
        }

        return false;
    }

    function getColorFromStatus(status) {
        switch (status) {
            case  1: return presenter.configuration.colors.above;
            case  0: return presenter.configuration.colors.correct;
            case -1: return presenter.configuration.colors.below;
        }

        return "black";
    }

    function half(v) { return parseInt(v / 2, 10); }

    function hookToDrawingAreas(addon) {
        var $addon = $('.' + addon);

        $addon.find('canvas').on('mousedown touchstart', function (){
            presenter.addons[addon] = true;
        });

        $addon.find('canvas').on('mouseup mouseleave touchend', function (){
            presenter.addons[addon] = false;
        });
    }

    var presenter = function() {};
    
    presenter.addons = {
        "addon_Drawing": false,
        "addon_Shape_Tracing": false,
        "addon_LearnPen": false
    };

    presenter.data = {
        $canvas: null,
        context: null,
        $img: null,

        sensorData: {
            below: 0,
            correct: 0,
            above: 0
        },

        sensorsDataHistory: [],

        isPreview: false,
        isIntervalOn: false,
        intervalId: null,
        isPaused: false
    };

    presenter.SENSOR = {
        'All': 'ALL',
        'Squeeze': 'SQUEEZE',
        'Pressure': 'PRESSURE',
        'Squeeze A': 'SQUEEZEA',
        'Squeeze B': 'SQUEEZEB',
        'Squeeze C': 'SQUEEZEC',
        DEFAULT: 'All'
    };
    // {Pie chart, Circle in circle, Four circles}
    presenter.GRAPH = {
        'Pie chart': 'PIE',
        'Circle in circle': 'CINC',
        'Four circles': '4CIRCLES',
        'Horizontal Bar': 'BAR',
        DEFAULT: 'Pie chart'
    };

    presenter.filteredDataCount = 0;

    presenter.ERROR_CODES = {
        R01: "Number of arguments in Correct range is different then 2",
        R02: "All values in Correct range has to be numeric",
        R03: "All values in Correct range has to be between 0 and 100",
        R04: "Second argument in Correct range hast to be bigger then the first",

        C01: "Number of argument is property Colors is different then 3",

        I01: "Property Data update interval has to be numeric",
        I02: "Value of property Data update interval has to be between 50 and 2000 ms",

        G01: "You cannot set sensor different then All and graph Circle in Circle or Four circles",

        CALC01: "Property Calculate from last values has to be integer",
        CALC02: "Property Calculate from last values has to be positive value"
    };

    function toPercent(val) { return parseInt(val / 1024 * 100); }
    
    function isDataNotNoise(element, index, array) {
        return element &gt;= 200;
    }


    function getCurrentDataFromSensor() {
        if(window.LearnPen) {
            var a = window.LearnPen.getA();
            var b = window.LearnPen.getB();
            var c = window.LearnPen.getC();
            var p = window.LearnPen.getP();
            var isValid = [a, b, c].some(isDataNotNoise);
            
            if(!isValid) {
                isValid = (p &gt;= 200);
            }

            return {
                isValid: isValid,
                a: toPercent(a),
                b: toPercent(b),
                c: toPercent(c),
                p: toPercent(p)
            };
        } else {
            return {
                isValid: false,
                a: 0,
                b: 0,
                c: 0,
                p: 0

//                isValid: true,
//                a: toPercent(Math.floor(Math.random() * 1000)),
//                b: toPercent(Math.floor(Math.random() * 1000)),
//                c: toPercent(Math.floor(Math.random() * 1000)),
//                p: toPercent(Math.floor(Math.random() * 1000))
            };            
        }
    }

    function getSensorHistory(historyArray, sensorsArray) {
        var noise = toPercent(200);

        function filterNoise(element) {
            return this.some(function (variable) {
               return element[variable] &gt; noise;
            }, this);
        }

        return historyArray.filter(filterNoise, sensorsArray);
    }

    function getValues() {
        var returnedData;
        if (presenter.configuration.calculateFromLastValues === 0) {
            returnedData = getSensorHistory(presenter.data.sensorsDataHistory, getSensorsConfiguration());
        } else {
            returnedData = getSensorHistory(getLastElements(presenter.data.sensorsDataHistory, getSensorsConfiguration(), presenter.configuration.calculateFromLastValues));
        }

        return returnedData;
    }

    function getSensorsConfiguration () {
        switch (presenter.configuration.sensor) {
            case presenter.SENSOR.All:
                return ['a', 'b', 'c', 'p'];
                break;
            case presenter.SENSOR.Pressure:
                return ['p'];
                break;
            case presenter.SENSOR.Squeeze:
                return ['a', 'b', 'c'];
                break;
            case presenter.SENSOR['Squeeze A']:
                return ['a'];
                break;
            case presenter.SENSOR['Squeeze B']:
                return ['b'];
                break;
            case presenter.SENSOR['Squeeze C']:
                return ['c'];
                break;
            default:
                return ['a', 'b', 'c', 'p'];
                break;
        }
    }

    function updateSensorDataHistory(data) {
        if (data.isValid) {
            presenter.filteredDataCount = 0;
            presenter.data.sensorsDataHistory.push(data);
        }

        if (presenter.configuration.calculateFromLastValues &gt; 0) {
            if(!data.isValid) {
                if (presenter.filteredDataCount &lt; presenter.configuration.calculateFromLastValues) {
                    presenter.filteredDataCount += 1;
                    presenter.data.sensorsDataHistory.push(data);
                }
            }
        }
    }

    function getStatus(v) {
        if (v &gt; presenter.configuration.range.end) {
            return 1;
        } else if (v &lt; presenter.configuration.range.start) {
            return -1;
        }

        return 0;
    }

    function updateResultDataFromSensorValue(value, result) {
        switch (getStatus(value)) {
            case  1: result.above++; break;
            case  0: result.correct++; break;
            case -1: result.below++; break;
        }
    }

    function prepareData() {
        var resultObject = {
            above: 0,
            correct: 0,
            below: 0,
            aStatus: 0,
            bStatus: 0,
            cStatus: 0,
            pStatus: 0
        };

        getValues().forEach(function(sensorDataObj, _, arr) {
            switch (presenter.configuration.sensor) {
                case presenter.SENSOR.All: updateResultDataFromSensorValue((sensorDataObj.a + sensorDataObj.b + sensorDataObj.c + sensorDataObj.p) / 4, resultObject); break;
                case presenter.SENSOR.Pressure: updateResultDataFromSensorValue(sensorDataObj.p, resultObject); break;
                case presenter.SENSOR.Squeeze: updateResultDataFromSensorValue((sensorDataObj.a + sensorDataObj.b + sensorDataObj.c) / 3, resultObject); break;
                case presenter.SENSOR['Squeeze A']: updateResultDataFromSensorValue(sensorDataObj.a, resultObject); break;
                case presenter.SENSOR['Squeeze B']: updateResultDataFromSensorValue(sensorDataObj.b, resultObject); break;
                case presenter.SENSOR['Squeeze C']: updateResultDataFromSensorValue(sensorDataObj.c, resultObject); break;
            }

            resultObject.aStatus += sensorDataObj.a / arr.length;
            resultObject.bStatus += sensorDataObj.b / arr.length;
            resultObject.cStatus += sensorDataObj.c / arr.length;
            resultObject.pStatus += sensorDataObj.p / arr.length;
        });

        resultObject.aStatus = getStatus(resultObject.aStatus);
        resultObject.bStatus = getStatus(resultObject.bStatus);
        resultObject.cStatus = getStatus(resultObject.cStatus);
        resultObject.pStatus = getStatus(resultObject.pStatus);

        return resultObject;
    }

    function validateRange(range) {
        if (ModelValidationUtils.isStringEmpty(range)) {
            return getCorrectObject({ start: 40, end: 80 });
        }

        var values = range.split(';');

        if (values.length !== 2) {
            return getErrorObject('R01');
        }

        for (var i=0; i&lt;values.length; i++) {
            if (isInteger(values[i])) {
                values[i] = parseInt(values[i], 10);
            } else {
                return getErrorObject('R02');
            }

            if (0 &gt; values[i] || values[i] &gt; 100) {
                return getErrorObject('R03');
            }
        }

        if (values[0] &gt;= values[1]) {
            return getErrorObject('R04');
        }

        return getCorrectObject({ start: values[0], end: values[1] });
    }

    function validateColors(colors) {
        if (ModelValidationUtils.isStringEmpty(colors)) {
            return getCorrectObject({ above: "red", correct: "green", below: "yellow" });
        }

        colors = colors.split(';');

        if (colors.length !== 3) {
            return getErrorObject('C01');
        }

        return getCorrectObject({ above: colors[0], correct: colors[1], below: colors[2] });
    }

    function validateInterval(interval) {
        if (ModelValidationUtils.isStringEmpty(interval)) {
            return getCorrectObject(100);
        }

        if (isInteger(interval)) {
            interval = parseInt(interval, 10);
        } else {
            return getErrorObject('I01');
        }

        if (interval &lt; 0 || interval &gt; 2000) {
            return getErrorObject('I02');
        }

        return getCorrectObject(interval);
    }

    function validateInteger(val) {
        if (ModelValidationUtils.isStringEmpty(val)) {
            return getCorrectObject(0);
        }

        if (isInteger(val)) {
            val = parseInt(val, 10);
        } else {
            return getErrorObject('CALC01');
        }

        if (val &lt; 0) {
            return getErrorObject('CALC02');
        }

        return getCorrectObject(val);
    }

    presenter.validateModel = function(model) {
        var validatedRange = validateRange(model.correctRange);
        if (validatedRange.errorCode) {
            return validatedRange;
        }

        var graph = ModelValidationUtils.validateOption(presenter.GRAPH, model.graphType);
        var sensor = ModelValidationUtils.validateOption(presenter.SENSOR, model.sensor);

        if ((graph === presenter.GRAPH['Circle in circle'] || graph === presenter.GRAPH['Four circles']) &amp;&amp; sensor !== presenter.SENSOR['All']) {
            return getErrorObject('G01');
        }

        var validatedColors = validateColors(model.colors);
        if (validatedColors.errorCode) {
            return validatedColors;
        }

        var validatedDataUpdateInterval = validateInterval(model.dataUpdateInterval);
        if (validatedDataUpdateInterval.errorCode) {
            return validatedDataUpdateInterval;
        }

        var validatedCalcFromLastValues = validateInteger(model.calculateFromLastValues);
        if (validatedCalcFromLastValues.errorCode) {
            return validatedCalcFromLastValues;
        }

        return {
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            ID: model.ID,
            width: parseInt(model.Width, 10),
            height: parseInt(model.Height, 10),

            isDisable: ModelValidationUtils.validateBoolean(model.isDisable),
            range: validatedRange.value,
            graphType: graph,
            sensor: sensor,
            colors: validatedColors.value,
            updateTime: validatedDataUpdateInterval.value,
            calculateFromLastValues: validatedCalcFromLastValues.value
        }
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "PageLoaded") {
            for (var addon in presenter.addons) {
                if (presenter.addons.hasOwnProperty(addon)) {
                    hookToDrawingAreas(addon);
                }
            }
        }
    };


    presenter.run = function(view, model) {
        presenter.view = view;
        presenterLogic(view, model, false);

        if (!presenter.configuration.isDisable) {
            presenter.record();
        }

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    function presenterLogic(view, model, isPreview) {
        presenter.$view = $(view);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        presenter.data.$img = presenter.$view.find('img');
        presenter.data.$canvas = presenter.$view.find('canvas');
        presenter.data.context = presenter.data.$canvas[0].getContext('2d');

        presenter.data.$canvas[0].width = presenter.configuration.width;
        presenter.data.$canvas[0].height = presenter.configuration.height;

        presenter.data.isPreview = isPreview;
    }

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) { return; }

        if (presenter.data.isIntervalOn) {
            clearInterval(presenter.data.intervalId);
            presenter.data.isIntervalOn = false;
        }
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);

        if (presenter.configuration.isValid) {
            presenter.displayCurrentData();
        }
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    function getRotateCoordinates(x, y, rad) {
        // Subtract midpoints, so that midpoint is translated to origin and add it in the end again
        return {
            x: round(x * Math.cos(rad) - y * Math.sin(rad), 2),
            y: round(x * Math.sin(rad) + y * Math.cos(rad), 2)
        }
    }

    function generateHorizontalBar(above, correct, below) {
        function drawRec(ctx, x, y, w, h, color, percent) {
            if(percent &gt; 0){
                ctx.beginPath();
                ctx.lineWidth="1";
                ctx.strokeStyle=color;
                ctx.rect(x, y, w, h);
                ctx.fillStyle = color;
                ctx.fill();
                ctx.stroke();
            }
        }

        function drawText(ctx, x, y, text) {
            if(text &gt; 0){
                ctx.font = "12px Calibri";
                ctx.textAlign = "center";
                ctx.fillStyle = "black";
                ctx.fillText(text + '%', x, y);
            }
        }

        var moduleHeight = presenter.configuration.height;

        var sum = above + correct + below;

        var text = [];
        text[0] = Math.round((below / sum) * 100);
        text[1] = Math.round((correct / sum) * 100);
        text[2] = Math.round(100 - (text[0] + text[1]));

        var widthA = Math.round(((text[0]/100)*presenter.configuration.width));
        var widthB = Math.round(((text[1]/100)*presenter.configuration.width));
        var widthC = Math.round(((text[2]/100)*presenter.configuration.width));
        var moduleWidth = widthA + widthB + widthC;

        var xA = 0;
        var xB = widthA;
        var xC = widthA + widthB;

        presenter.data.context.clearRect(0, 0, presenter.configuration.width, presenter.configuration.height);

        drawRec(presenter.data.context, xA, Math.round(moduleHeight/4), widthA, Math.round(moduleHeight/2), presenter.configuration.colors.below, text[0]);
        drawRec(presenter.data.context, xB, Math.round(moduleHeight/4), widthB, Math.round(moduleHeight/2), presenter.configuration.colors.correct, text[1]);
        drawRec(presenter.data.context, xC, Math.round(moduleHeight/4), widthC, Math.round(moduleHeight/2), presenter.configuration.colors.above, text[2]);

        var xTextA = xA + (widthA/2);
        var xTextB = xB + (widthB/2);
        var xTextC = xC + (widthC/2);

        if(text[0] &gt; 0 &amp;&amp; xTextA &lt; 10){
            xTextA = 10;
        }
        if(text[2] &gt; 0 &amp;&amp; xTextC &gt; (moduleWidth-11)){
            xTextC = moduleWidth-11;
        }
        if(text[0] == 0 &amp;&amp; xTextB &lt; 10){
            xTextB = 10;
        }
        if(text[2] == 0 &amp;&amp; xTextB &gt; (moduleWidth-10)){
            xTextB = moduleWidth-10;
        }
        if(text[0] &gt; 0 &amp;&amp; xTextB &lt; (xTextA+19)){
            xTextB = xTextA+19;
        }else if(text[2] &gt; 0 &amp;&amp; xTextB &gt; (xTextC-20)){
            xTextB = xTextC-20;
        }else{} // 10, 19, 20.. - width of text

        drawText(presenter.data.context, xTextA, Math.round(moduleHeight/4)-3, text[0]);
        drawText(presenter.data.context, xTextB, Math.round(moduleHeight/4)-3, text[1]);
        drawText(presenter.data.context, xTextC, Math.round(moduleHeight/4)-3, text[2]);
    }

    function generatePieChart(above, correct, below) {
        function drawArc(ctx, x, y, r, start, end, color) {
            ctx.beginPath();
            ctx.moveTo(x, y);
            ctx.arc(x, y, r, start, end, false);
            ctx.closePath();
            ctx.fillStyle = color;
            ctx.fill();
        }

        function drawText(ctx, x, y, r, start, end, text) {
            var halfAngle = (end - start) / 2;
            var coordinates = getRotateCoordinates(r / 2, 0, halfAngle + start);

            ctx.font = "20px Calibri";
            ctx.textAlign = "center";
            ctx.fillStyle = "black";
            ctx.fillText(text + '%', coordinates.x + x, coordinates.y + y);
        }

        var sum = above + correct + below;
        var centerX = Math.floor(presenter.configuration.width / 2);
        var centerY = Math.floor(presenter.configuration.height / 2);
        var radius = Math.min(presenter.configuration.width, presenter.configuration.height) / 2;

        var sep = [];
        sep[0] = 0;
        sep[1] = (below / sum) * 2 * Math.PI;
        sep[2] = ((below + correct) / sum) * 2 * Math.PI;
        sep[3] = 2 * Math.PI;

        var text = [];
        text[0] = round((below / sum) * 100, 2);
        text[1] = round((correct / sum) * 100, 2);
        text[2] = round(100 - (text[0] + text[1]), 2);

        presenter.data.context.clearRect(0, 0, presenter.configuration.width, presenter.configuration.height);

        drawArc(presenter.data.context, centerX, centerY, radius, sep[0], sep[1], presenter.configuration.colors.above);
        drawArc(presenter.data.context, centerX, centerY, radius, sep[1], sep[2], presenter.configuration.colors.correct);
        drawArc(presenter.data.context, centerX, centerY, radius, sep[2], sep[3], presenter.configuration.colors.below);

        drawText(presenter.data.context, centerX, centerY, radius, sep[0], sep[1], text[0]);
        drawText(presenter.data.context, centerX, centerY, radius, sep[1], sep[2], text[1]);
        drawText(presenter.data.context, centerX, centerY, radius, sep[2], sep[3], text[2]);
    }

    function drawCircle(ctx, x, y, r, color) {
        ctx.beginPath();
        ctx.arc(x, y, r, 0, 2 * Math.PI, false);
        ctx.fillStyle = color;
        ctx.fill();
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'white';
        ctx.stroke();
    }

    function generateFourCircles(aStatus, bStatus, cStatus, pStatus) {
        function getCircleData(_x, _y, _r, _c) { return { x: _x, y: _y, r: _r, color: _c }; }

        var min = Math.min(presenter.configuration.width, presenter.configuration.height);
        var smallRadius = parseInt(min / 8, 10);
        var bigRadius = parseInt(min / 3, 10);

        var offset = half(Math.abs(presenter.configuration.width - presenter.configuration.height));

        var a, b, c, p;

        var aColor = getColorFromStatus(aStatus);
        var bColor = getColorFromStatus(bStatus);
        var cColor = getColorFromStatus(cStatus);
        var pColor = getColorFromStatus(pStatus);

        if (presenter.configuration.height &gt; presenter.configuration.width) {
            a = getCircleData(smallRadius, offset + smallRadius, smallRadius, aColor);
            b = getCircleData(min - smallRadius, offset + smallRadius, smallRadius, bColor);
            c = getCircleData(half(min), offset + min - smallRadius, smallRadius, cColor);
            p = getCircleData(half(min), offset + parseInt(min * 0.41, 10), bigRadius, pColor);
        } else {
            a = getCircleData(offset + smallRadius, smallRadius, smallRadius, aColor);
            b = getCircleData(offset + min - smallRadius, smallRadius, smallRadius, bColor);
            c = getCircleData(offset + half(min), min - smallRadius, smallRadius, cColor);
            p = getCircleData(offset + half(min), parseInt(min * 0.41, 10), bigRadius, pColor);
        }

        drawCircle(presenter.data.context, a.x, a.y, a.r, a.color);
        drawCircle(presenter.data.context, b.x, b.y, b.r, b.color);
        drawCircle(presenter.data.context, c.x, c.y, c.r, c.color);
        drawCircle(presenter.data.context, p.x, p.y, p.r, p.color);
    }

    function generateCircleInCircle(aStatus, bStatus, cStatus, pStatus) {
        var centerX = Math.floor(presenter.configuration.width / 2);
        var centerY = Math.floor(presenter.configuration.height / 2);

        var radiusOuter = Math.min(presenter.configuration.width, presenter.configuration.height) / 2;
        var radiusInner = parseInt(radiusOuter / 2, 10);

        var colorOuter = getColorFromStatus(Math.round((aStatus + bStatus + cStatus) / 3));
        var colorInner = getColorFromStatus(pStatus);

        drawCircle(presenter.data.context, centerX, centerY, radiusOuter, colorOuter);
        drawCircle(presenter.data.context, centerX, centerY, radiusInner, colorInner);
    }

    presenter.displayCurrentData = function() {
        var shouldUpdate = shouldGetDataFromSensors();
        if (shouldUpdate){
            var dataFromSensors = getCurrentDataFromSensor();
            updateSensorDataHistory(dataFromSensors);
        }

        var data = presenter.getData();
        presenter.displayData(data);
    };

    presenter.getData = function getData() {
        return presenter.data.isPreview ? {
            above: 1,
            correct: 1,
            below: 1,
            aStatus: 0,
            bStatus: 0,
            cStatus: 0,
            pStatus: 0
        } : prepareData();
    };

    presenter.displayData = function (data) {
        switch (presenter.configuration.graphType) {
            case presenter.GRAPH['Pie chart']: generatePieChart(data.above, data.correct, data.below); break;
            case presenter.GRAPH['Four circles']: generateFourCircles(data.aStatus, data.bStatus, data.cStatus, data.pStatus); break;
            case presenter.GRAPH['Circle in circle']: generateCircleInCircle(data.aStatus, data.bStatus, data.cStatus, data.pStatus); break;
            case presenter.GRAPH['Horizontal Bar']: generateHorizontalBar(data.above, data.correct, data.below); break;
            default: break;
        }

        // copy chart to img
        presenter.data.$img.attr("src", presenter.data.$canvas[0].toDataURL("image/png"));
    };

    presenter.reset = function() {
        var addon;
        presenter.data.sensorsDataHistory = [];
        presenter.displayData(presenter.getData());
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);

        for (addon in presenter.addons) {
            if (presenter.addons.hasOwnProperty(addon)) {
                hookToDrawingAreas(addon);
            }
        }
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.record = function() {
        if (presenter.configuration.isDisable) { return false; }

        if (!presenter.data.isIntervalOn) {
            presenter.data.isIntervalOn = true;
            presenter.data.intervalId = setInterval(presenter.displayCurrentData, presenter.configuration.updateTime);
        }
    };

    presenter.pause = function() {
        if (presenter.configuration.isDisable) { return false; }

        if (presenter.data.isIntervalOn) {
            clearInterval(presenter.data.intervalId);
            presenter.data.isIntervalOn = false;
        }
    };

    presenter.stop = function() {
        if (presenter.configuration.isDisable) { return false; }

        if (presenter.data.isIntervalOn) {
            clearInterval(presenter.data.intervalId);
            presenter.data.isIntervalOn = false;
        }
        presenter.reset();
    };

    presenter.executeCommand = function(name, params) {
        if (!presenter.configuration.isValid) { return false; }

        Commands.dispatch({
            "reset": presenter.reset,
            "show": presenter.show,
            "hide": presenter.hide,
            "record": presenter.record,
            "stop": presenter.stop,
            "pause": presenter.pause
        }, name, params, presenter);
    };

    presenter.getState = function() {
        presenter.pause();

        return JSON.stringify({
            sensorData: presenter.data.sensorData,
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) { return; }

        var parsedState = JSON.parse(state);

        presenter.data.sensorData = parsedState.sensorData;
        presenter.configuration.isVisible = parsedState.isVisible;

        presenter.record();
    };

    presenter.setShowErrorsMode = function() {
        presenter.pause();
    };

    presenter.setWorkMode = function() {
        presenter.record();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Lesson_Error_Counter" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
    </model>
<css>.addon_Lesson_Error_Counter {
    text-align: center;
    border-radius: 5px;
    border: 2px solid black;
    color: red;
}
</css><view/><preview>6
</preview><presenter>function AddonLesson_Error_Counter_create() {
    var presenter = function () { };

    presenter.playerController = null;

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.scoreService = controller.getScore();
    };

    presenter.countErrors = function () {
        var presentation = presenter.playerController.getPresentation(),
            errorCount = 0, pageScore;


        for(var i = 0; i &lt; presentation.getPageCount(); i++){
            var page = presentation.getPage(i);

            if(page.isReportable() &amp;&amp; page.isVisited()){
                pageScore = presenter.scoreService.getPageScoreById(page.getId());
                errorCount += pageScore.errorCount;
            }
        }

        presenter.$view.text(errorCount);
    };

    presenter.sanitizeModel = function (model) {
        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);

        return {
            isVisibleByDefault: isVisible,
            isVisible: isVisible
        };
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.configuration = presenter.sanitizeModel(model);

        presenter.setVisibility(presenter.configuration.isVisibleByDefault || isPreview);

        if (!isPreview) {
            presenter.countErrors();
        }
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };
    
    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        if (!state) {
            return;
        }

        var parsedState = JSON.parse(state);

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(parsedState.isVisible);
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.reset = function () {
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Lesson_Progress" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Show Progress Bar" name="Show_Progress_Bar" nameLabel="Lesson_Progress_property_show_progress_bar" type="boolean"/>
        <property displayName="Show Checks" name="Show_Checks" nameLabel="Lesson_Progress_property_show_checks" type="boolean"/>
        <property displayName="Show Correct Answers" name="Show_Correct_Answers" nameLabel="Lesson_Progress_property_show_correct_answers" type="boolean"/>
        <property displayName="Show Errors" name="Show_Errors" nameLabel="Lesson_Progress_property_show_errors" type="boolean"/>
        <property displayName="Show Mistakes" name="Show_Mistakes" nameLabel="Lesson_Progress_property_show_mistakes" type="boolean"/>
        <property displayName="Show All Answers" name="Show_All_Answers" nameLabel="Lesson_Progress_property_show_all_answers" type="boolean"/>
        <property displayName="Calculate score on page change" name="Calculate_Score_On_Page_Change" nameLabel="Lesson_Progress_property_calculate_score_on_page_change" type="boolean"/>
	</model>
<css>.lesson-progress-container .progress-bar-container.hidden, .lesson-progress-container .progress-box.hidden {
    display: none;
}

.lesson-progress-container {
    width: 100%;
    height: 100%;

}

.lesson-progress-container .progress-bar-container {
    width: 200px;
    height: 50px;
    position: relative;
    border-radius: 5px;
    border: 2px solid #02789F;
    padding: 2px;
    display: inline-block;
    float: left;
}

.lesson-progress-container .progress-bar {
    background-color: #3CC6CD;
    position: absolute;
    height: 50px;
    border-radius: 5px;
}

.lesson-progress-container .progress-text {
    line-height: 50px;
    position: absolute;
    width: 100%;
    text-align: center;
}

.lesson-progress-container .progress-box .text {
    min-height: 35px;
}

.lesson-progress-container .progress-box {
    line-height: 14px;
    display: inline-block;
    width: 50px;
    height: 50px;
    padding: 3px;
    border: 1px solid #02789F;
    margin-left: 5px;
    text-align: center;
    border-radius: 5px;
    font-size: 10px;
    float: left;
}
</css><view>&lt;div class="lesson-progress-container"&gt;
    &lt;div class="progress-bar-container hidden"&gt;
        &lt;div class="progress-bar"&gt;&lt;/div&gt;
        &lt;div class="progress-text"&gt;0%&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box correct-answers hidden"&gt;
        &lt;div class="text"&gt;Correct&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box max-score hidden"&gt;
        &lt;div class="text"&gt;Max Score&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box mistakes hidden"&gt;
        &lt;div class="text"&gt;Mistakes&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box checks hidden"&gt;
        &lt;div class="text"&gt;Checks&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box errors hidden"&gt;
        &lt;div class="text"&gt;Errors&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="lesson-progress-container"&gt;
    &lt;div class="progress-bar-container hidden"&gt;
        &lt;div class="progress-bar" style="width: 50%;"&gt;&lt;/div&gt;
        &lt;div class="progress-text"&gt;50%&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box correct-answers hidden"&gt;
        &lt;div class="text"&gt;Correct&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box max-score hidden"&gt;
        &lt;div class="text"&gt;Max Score&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box mistakes hidden"&gt;
        &lt;div class="text"&gt;Mistakes&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box checks hidden"&gt;
        &lt;div class="text"&gt;Checks&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box errors hidden"&gt;
        &lt;div class="text"&gt;Errors&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLesson_Progress_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('ShowErrors', this);
    };

    presenter.onEventReceived = function(eventName, _) {
        if (eventName == 'ShowErrors') {
            presenter.setShowErrorsMode();
        }
    };

    presenter.ERROR_CODES = {
    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        return {
            showProgressBar: ModelValidationUtils.validateBoolean(model['Show_Progress_Bar']),
            showChecks: ModelValidationUtils.validateBoolean(model['Show_Checks']),
            showErrors: ModelValidationUtils.validateBoolean(model['Show_Errors']),
            showMistakes: ModelValidationUtils.validateBoolean(model['Show_Mistakes']),
            showMaxScore: ModelValidationUtils.validateBoolean(model['Show_All_Answers']),
            showCorrectAnswers: ModelValidationUtils.validateBoolean(model['Show_Correct_Answers']),
            calculateScoreOnPageChange: ModelValidationUtils.validateBoolean(model['Calculate_Score_On_Page_Change'])
        }
    };

    function runLogic(view, model, isPreview) {
        presenter.$view = $(view);
        presenter.$progressBarContainer = presenter.$view.find('.progress-bar-container');
        presenter.$progressBar = presenter.$view.find('.progress-bar');
        presenter.$progressText = presenter.$view.find('.progress-text');
        presenter.$checks = presenter.$view.find('.checks');
        presenter.$errors = presenter.$view.find('.errors');
        presenter.$mistakes = presenter.$view.find('.mistakes');
        presenter.$maxScore = presenter.$view.find('.max-score');
        presenter.$correctAnswers = presenter.$view.find('.correct-answers');

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);

        removeHidden(presenter.configuration.showProgressBar, presenter.$progressBarContainer);
        removeHidden(presenter.configuration.showChecks, presenter.$checks);
        removeHidden(presenter.configuration.showErrors, presenter.$errors);
        removeHidden(presenter.configuration.showMistakes, presenter.$mistakes);
        removeHidden(presenter.configuration.showMaxScore, presenter.$maxScore);
        removeHidden(presenter.configuration.showCorrectAnswers, presenter.$correctAnswers);

        if (presenter.configuration.calculateScoreOnPageChange &amp;&amp; !isPreview) {
            presenter.setShowErrorsMode();
        }
    }

    function removeHidden(shouldRemove, $element) {
        if (shouldRemove) {
            $element.removeClass('hidden');
        } else {
            $element.addClass('hidden');
        }
    }

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    function getLessonScore() {
        var sumOfScore = 0.0, sumOfErrors = 0, sumOfChecks = 0,
            sumOfMaxScore = 0.0,
            sumOfScaledScore = 0.0,
            sumOfMistakes = 0,
            sumOfWeights = 0,
            pageScaledScore = 0,
            count = 0, i, page, score,
            paginatedResults = [];
        var presentation = presenter.playerController.getPresentation();
        var scoreService = presenter.playerController.getScore();

        for (var i = 0; i &lt; presentation.getPageCount(); i++) {
            var page = presentation.getPage(i);

            if (page.isReportable()) {
                score = scoreService.getPageScoreById(page.getId());

                if (score['maxScore']) {
                    pageScaledScore = score['score'] / score['maxScore'];
                } else {
                    pageScaledScore = page.isVisited() ? 1 : 0;
                }

                var _weight = page.getPageWeight();
                var weight =  !_weight &amp;&amp; _weight !== 0 ? 1 : _weight;
                sumOfScaledScore += pageScaledScore * weight;
                sumOfScore += score.score;
                sumOfErrors += score.errorCount;
                sumOfChecks += score.checkCount;
                sumOfMaxScore += score.maxScore;
                sumOfMistakes += score.mistakeCount;
                sumOfWeights += weight;
                count += 1;
            }
        }

        var scaledScore = 0;
        if (count &gt; 0) {
            if (sumOfWeights) {
                scaledScore = Math.round((sumOfScaledScore / sumOfWeights) * 100);
            } else {
                scaledScore = 1;
            }
        }

        return {
           progress: parseInt(scaledScore, 10),
           sumOfMaxScore: scoreService.getMaxScore(),
           sumOfMistakes: sumOfMistakes,
           sumOfErrors: sumOfErrors,
           sumOfChecks: sumOfChecks,
           sumOfScores: sumOfScore
       };
    }

    presenter.setShowErrorsMode = function(){
        var lessonScore = getLessonScore();
        if (presenter.configuration.showProgressBar) {
            presenter.$progressBar.css('width', lessonScore.progress + '%');
            presenter.$progressText.html(lessonScore.progress + '%');
        }

        if (presenter.configuration.showChecks) {
            presenter.$checks.find('.value').html(lessonScore.sumOfChecks);
        }

        if (presenter.configuration.showMistakes) {
            presenter.$mistakes.find('.value').html(lessonScore.sumOfMistakes);
        }

        if (presenter.configuration.showErrors) {
            presenter.$errors.find('.value').html(lessonScore.sumOfErrors);
        }

        if (presenter.configuration.showMaxScore) {
            presenter.$maxScore.find('.value').html(lessonScore.sumOfMaxScore);
        }

        if (presenter.configuration.showCorrectAnswers) {
            presenter.$correctAnswers.find('.value').html(lessonScore.sumOfScores);
        }
    };

    presenter.setWorkMode = function(){
    };

    presenter.show = function() {
        presenter.$view.show();
    };

    presenter.hide = function() {
        presenter.$view.hide();
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show' : presenter.show,
            'hide' : presenter.hide,
            'getLessonProgress' : presenter.getLessonProgress,
            'getLessonScore' : presenter.getLessonScore,
            'getLessonMaxScore' : presenter.getLessonMaxScore,
            'getLessonMistakes' : presenter.getLessonMistakes,
            'getLessonChecks' : presenter.getLessonChecks,
            'getLessonErrors' : presenter.getLessonErrors
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getLessonProgress = function() {
        return getLessonScore().progress;
    };

    presenter.getLessonScore = function() {
        return getLessonScore().sumOfScores;
    };

    presenter.getLessonMaxScore = function() {
        return getLessonScore().sumOfMaxScore;
    };

    presenter.getLessonMistakes = function() {
        return getLessonScore().sumOfMistakes;
    };

    presenter.getLessonChecks = function() {
        return getLessonScore().sumOfChecks;
    };

    presenter.getLessonErrors = function() {
        return getLessonScore().sumOfErrors;
    };

    presenter.reset = function(){
    };

    presenter.getState = function(){
    };

    presenter.setState = function(state){
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = $.extend(true, upgradedModel, model);

        if (!upgradedModel['Calculate_Score_On_Page_Change']) {
            upgradedModel['Calculate_Score_On_Page_Change'] = 'False';
        }

        return upgradedModel;
    };

    return presenter;
}


</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Lesson_Score_Counter" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
<model>
    <property displayName="Display Mode" name="DisplayMode" nameLabel="Lesson_Score_Counter_property_display_mode" type="{Fraction (Score/Max Score),Score,Max Score,Percentage}"/>
</model>
<css>.lesson-score-counter-wrapper {

}

.lesson-score-counter-wrapper &gt; div.hidden {
    display: none;
}

.lesson-score-counter-wrapper &gt; div {
    border: 1px solid #002266;
    border-radius: 4px;
    background-color: #fafafa;
    box-shadow: 1px 1px 1px #005599;
    width: 50px;
    height: 50px;
    text-align: center;
    line-height: 50px;
    font-size: 12px;
}

.lesson-score-counter-wrapper .fraction &gt; div {
    text-align: left;
    display: inline-block;
}
</css><view>&lt;div class="lesson-score-counter-wrapper"&gt;
    &lt;div class="fraction hidden"&gt;
        &lt;div class="score"&gt;
            0
        &lt;/div&gt;
        &lt;div class="separator"&gt;
            /
        &lt;/div&gt;
        &lt;div class="max-score"&gt;
            0
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="score hidden"&gt;
        0
    &lt;/div&gt;
    &lt;div class="max-score hidden"&gt;
        0
    &lt;/div&gt;
    &lt;div class="percentage hidden"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="lesson-score-counter-wrapper"&gt;
    &lt;div class="fraction hidden"&gt;
        &lt;div class="score"&gt;
            0
        &lt;/div&gt;
        &lt;div class="separator"&gt;
            /
        &lt;/div&gt;
        &lt;div class="max-score"&gt;
            0
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="score hidden"&gt;
        0
    &lt;/div&gt;
    &lt;div class="max-score hidden"&gt;
        0
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLesson_Score_Counter_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.isVisible = true;
    presenter.score = 0;
    presenter.maxScore = 0;

    presenter.DISPLAY_MODE = {
        FRACTION: 1,
        SCORE: 2,
        MAX_SCORE: 3,
        PERCENTAGE: 4
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.onEventReceived = function(eventName) {
    };

    presenter.createEventData = function (score) {
    };

    presenter.sendEvent = function(eventName, eventData) {
        presenter.eventBus.sendEvent(eventName, eventData);
    };

    presenter.ERROR_CODES = {

    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        return {
            'isVisible' : ModelValidationUtils.validateBoolean(model["Is Visible"]),
            'addonID' : model['ID'],
            'displayMode' : getDisplayMode(model['DisplayMode'])
        }
    };

    function getDisplayMode(rawDisplayMode) {
        if (rawDisplayMode == 'Fraction (Score/Max Score)' || rawDisplayMode == '') { // when dropdown hasn't been changed it gives you empty string
            return presenter.DISPLAY_MODE.FRACTION;
        } else if (rawDisplayMode == 'Score'){
            return presenter.DISPLAY_MODE.SCORE;
        } else if (rawDisplayMode == 'Max Score') {
            return presenter.DISPLAY_MODE.MAX_SCORE;
        } else if (rawDisplayMode == 'Percentage') {
            return presenter.DISPLAY_MODE.PERCENTAGE;
        }
    }

    function runLogic(view, model, isPreview) {

        presenter.configuration = presenter.validateModel(model);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.$view = $(view);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        presenter.$fractionWrapper = presenter.$view.find('.lesson-score-counter-wrapper &gt; .fraction');
        presenter.$scoreWrapper = presenter.$view.find('.lesson-score-counter-wrapper &gt; .score');
        presenter.$maxScoreWrapper = presenter.$view.find('.lesson-score-counter-wrapper &gt; .max-score');
        presenter.$percentageWrapper = presenter.$view.find('.lesson-score-counter-wrapper &gt; .percentage');

        if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.FRACTION) {
            toggleBoxVisibility(presenter.$fractionWrapper);
        } else if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.SCORE) {
            toggleBoxVisibility(presenter.$scoreWrapper);
        } else if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.MAX_SCORE) {
            toggleBoxVisibility(presenter.$maxScoreWrapper);
        } else if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.PERCENTAGE) {
            toggleBoxVisibility(presenter.$percentageWrapper);
        }

        if (!isPreview) {
            updateValue();
        }
    }

    function updateValue(scoreValue, maxScoreValue) {
        if (presenter.playerController) {
            var score = scoreValue == undefined ? 0 : scoreValue,
                maxScore = maxScoreValue == undefined ? 0 : maxScoreValue;

            var scoreService = presenter.playerController.getScore();

            var score = scoreService.getTotalScore(),
                maxScore = scoreService.getMaxScore();

            if (maxScore &gt; 0) {
                presenter.$percentageWrapper.html(parseInt(((score/maxScore) * 100), 10) + '%');
            }
            presenter.$fractionWrapper.find('.score').html(score);
            presenter.$fractionWrapper.find('.max-score').html(maxScore);
            presenter.$scoreWrapper.html(score);
            presenter.$maxScoreWrapper.html(maxScore);
            presenter.score = score;
            presenter.maxScore = maxScore;
        }
    }

    function toggleBoxVisibility($element) {
        presenter.$view.find('.lesson-score-counter-wrapper &gt; div').addClass('hidden');
        $element.removeClass('hidden');
    }

    presenter.run = function(view, model) {
        runLogic(view, model, false);
    };

    presenter.setShowErrorsMode = function() {
    };

    presenter.setWorkMode = function() {};

    presenter.reset = function() {
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getErrorCount = function() {
        return 0;
    };

    presenter.getMaxScore = function() {
        return 0;
    };

    presenter.getScore = function() {
        return 0;
    };

    presenter.getState = function() {
        return JSON.stringify({
            'isVisible' : presenter.isVisible,
            'score' : presenter.score,
            'maxScore' : presenter.maxScore
        });
    };

    presenter.setState = function(state) {
        var parsed = JSON.parse(state);
        presenter.isVisible = parsed.isVisible;

        updateValue(parsed.score, parsed.maxScore);

        presenter.setVisibility(presenter.isVisible);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Limited_Show_Answers" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isLocalized="true" name="Text" nameLabel="Limited_Show_Answers_property_text" type="string"/>
        <property isLocalized="true" name="Text selected" nameLabel="Limited_Show_Answers_property_text_selected" type="string"/>
        <property name="Increment check counter" nameLabel="Limited_Show_Answers_property_increment_check_counter" type="boolean"/>
        <property name="Increment mistake counter" nameLabel="Limited_Show_Answers_property_increment_mistake_counter" type="boolean"/>
        <property name="worksWith" nameLabel="Limited_Show_Answers_property_works_with" type="text"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="choice_speech_texts" type="staticlist">

            <property name="Selected" nameLabel="Limited_Show_Answers_speech_text_choice_item_selected" type="staticrow">
                <property name="Selected" nameLabel="Limited_Show_Answers_speech_text_choice_item_selected" type="string"/>
            </property>

            <property name="Block edit" nameLabel="Limited_Show_Answers_speech_text_check_answers_button_edit_block" type="staticrow">
                <property name="Block edit" nameLabel="Limited_Show_Answers_speech_text_check_answers_button_edit_block" type="string"/>
            </property>

            <property name="No block edit" nameLabel="Limited_Show_Answers_speech_text_check_answers_button_no_edit_block" type="staticrow">
                <property name="No block edit" nameLabel="Limited_Show_Answers_speech_text_check_answers_button_no_edit_block" type="string"/>
            </property>

        </property>
    </model>
<css>.limited-show-answers-wrapper,
.limited-show-answers-wrapper .limited-show-answers-container,
.limited-show-answers-wrapper .limited-show-answers-container .limited-show-answers-button {
    width: 100%;
    height: 100%;
}

.limited-show-answers-wrapper .limited-show-answers-container .limited-show-answers-button {
    background: url('resources/show-answers-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}
</css><view>&lt;div class="limited-show-answers-wrapper"&gt;
    &lt;div class="limited-show-answers-container"&gt;
        &lt;div class="limited-show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="limited-show-answers-wrapper"&gt;
    &lt;div class="limited-show-answers-container"&gt;
        &lt;div class="limited-show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLimited_Show_Answers_create() {
    var presenter = function () {
    };

    presenter.playerController = null;
    presenter.eventBus = null;
    var isWCAGOn = false;

    function getSpeechTextProperty(rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function getTextVoiceObject(text, lang) {
        return {
            text: text,
            lang: lang
        };
    }

    presenter.EVENTS = {
        SHOW_ANSWERS: 'LimitedShowAnswers',
        HIDE_ANSWERS: 'LimitedHideAnswers'
    };

    presenter.EVENTS_MAP = {
        LimitedShowAnswers: "ShowAnswers",
        LimitedHideAnswers: "HideAnswers"
    };

    presenter.keyboardController = function (keycode) {
        if (keycode === 13) {
            presenter.$button.click();
            if (isWCAGOn) {
                if (presenter.configuration.isSelected) {
                    speak([getTextVoiceObject(presenter.speechTexts.editBlock)]);
                } else {
                    speak([getTextVoiceObject(presenter.speechTexts.noEditBlock)]);
                }
            }
        }
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.sendEvent = function (eventName) {
        if(!presenter.eventBus) return;
        var eventData = {
            'value': eventName,
            'source': presenter.configuration.addonID,
            'item': JSON.stringify(presenter.configuration.worksWithModulesList)
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);

        if (eventName === "LimitedShowAnswers") {
            presenter.eventBus.sendEvent('LimitedShowAnswers', eventData);
        } else if (eventName === "LimitedHideAnswers") {
            presenter.eventBus.sendEvent('LimitedHideAnswers', eventData);
        }

        presenter.sendEventToWorksWithModules(eventName);
    };

    presenter.sendEventToWorksWithModules = function (eventName) {
        presenter.configuration.worksWithModulesList.forEach(function (moduleId) {
            var module = player.getPlayerServices().getModule(moduleId);
            if (module &amp;&amp; module.onEventReceived) {
                try {
                    module.onEventReceived(presenter.EVENTS_MAP[eventName]);
                }catch (e) {
                    console.error(e);
                }
            }
        });
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.getWorksWithModulesList = function () {
        return presenter.configuration.worksWithModulesList.slice();
    };

    presenter.validateModel = function (model) {
        presenter.setSpeechTexts(model['speechTexts']);

        var modelValidator = new ModelValidator();
        var validatedModel = modelValidator.validate(model, [
            ModelValidators.utils.FieldRename("Is Visible", "isVisible", ModelValidators.Boolean("isVisible")),
            ModelValidators.utils.FieldRename("Text", "text", ModelValidators.String("text", {default: ""})),
            ModelValidators.utils.FieldRename("Text selected", "textSelected", ModelValidators.String("textSelected", {default: ""})),
            ModelValidators.utils.FieldRename("ID", "addonID", ModelValidators.DumbString("addonID")),
            ModelValidators.utils.FieldRename("Increment check counter", "enableCheckCounter", ModelValidators.Boolean("enableCheckCounter")),
            ModelValidators.utils.FieldRename("Increment mistake counter", "enableMistakeCounter", ModelValidators.Boolean("enableMistakeCounter")),
            ModelValidators.utils.FieldRename("Is Tabindex Enabled", "isTabindexEnabled", ModelValidators.Boolean("isTabindexEnabled")),
            ModelValidators.String("worksWith", {default: ""})
        ]);

        if (validatedModel.isValid) {
            validatedModel.value.isSelected = false;
            validatedModel.value.isEnabled = true;
            validatedModel.value.worksWithModulesList = validatedModel.value.worksWith.split("\n")
                .map(function (value) {
                    return value.trim();
                })
                .filter(function (value) {
                    return value !== "";
                })
                .filter(function (value, index, self) { //Unique elements
                    return self.indexOf(value) === index;
                });
        }

        return validatedModel;

    };

    presenter.setSpeechTexts = function (speechTexts) {
        presenter.speechTexts = {
            selected: 'Selected',
            editBlock: 'Exercise edition is blocked',
            noEditBlock: 'Exercise edition is not blocked'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            selected: getSpeechTextProperty(speechTexts['Selected']['Selected'], presenter.speechTexts.selected),
            editBlock: getSpeechTextProperty(speechTexts['Block edit']['Block edit'], presenter.speechTexts.editBlock),
            noEditBlock: getSpeechTextProperty(speechTexts['No block edit']['No block edit'], presenter.speechTexts.noEditBlock)
        };
    };

    presenter.handleClick = function () {
        var text, eventName;

        presenter.configuration.isSelected = !presenter.configuration.isSelected;

        if (presenter.configuration.isSelected) {
            text = presenter.configuration.textSelected;
            eventName = presenter.EVENTS.SHOW_ANSWERS;
            presenter.$wrapper.addClass('selected');

            if (presenter.configuration.enableCheckCounter) {
                presenter.playerController.getCommands().incrementCheckCounter();
            }

            if (presenter.configuration.enableMistakeCounter) {
                presenter.playerController.getCommands().increaseMistakeCounter();
            }
        } else {
            text = presenter.configuration.text;
            eventName = presenter.EVENTS.HIDE_ANSWERS;
            presenter.$wrapper.removeClass('selected');
        }

        presenter.$button.text(text);
        presenter.sendEvent(eventName);
    };

    presenter.connectClickAction = function () {
        presenter.$button.on('click', function (eventData) {
            eventData.stopPropagation();
            if (presenter.configuration.isEnabled)
                presenter.handleClick();
        });
    };

    presenter.connectKeyDownAction = function () {
        presenter.$view.on('keydown', function (eventData) {
            if (eventData.which === 13) {
                eventData.stopPropagation();
                presenter.handleClick();
            }
        });
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.configuration = presenter.validateModel(model).value;
        presenter.$view = $(view);

        presenter.$button = presenter.$view.find('.limited-show-answers-button');
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper = presenter.$view.find('.limited-show-answers-wrapper');

        if (presenter.configuration.isTabindexEnabled) {
            presenter.$wrapper.attr('tabindex', '0');
        }

        if (!isPreview) {
            presenter.setVisibility(presenter.configuration.isVisible);
            presenter.connectClickAction();
            presenter.connectKeyDownAction();
            presenter.eventBus.addEventListener('ShowAnswers', presenter);
            presenter.eventBus.addEventListener('HideAnswers', presenter);
            presenter.eventBus.addEventListener('LimitedHideAnswers', presenter);
        }
    };

    presenter.run = function (view, model) {
        presenter.view = view;
        presenter.presenterLogic(view, model, false);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }
        presenter.$button.off();
        presenter.$view.off();

        presenter.$button = null;
        presenter.$wrapper = null;
        presenter.$view = null;
        presenter.view = null;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.isEventSourceLocal = function (eventData) {
        return eventData.source === presenter.configuration.addonID;
    }

    presenter.eventItemsContainAllWorksWithModules = function (eventDataItems) {
        const moduleList = presenter.configuration.worksWithModulesList;
        for (let i in moduleList) {
            if (!JSON.parse(eventDataItems).includes(moduleList[i])) {
                return false;
            }
        }
        return true;
    }

    presenter.handleLimitedHideAnswers = function (eventName, eventData) {
        if (presenter.isEventSourceLocal(eventData)) return;

        if (eventData.item) {
            if (eventData.item.includes(presenter.configuration.addonID)) {
                presenter.sendEvent(eventName);
                presenter.reset();
            } else if (presenter.eventItemsContainAllWorksWithModules(eventData.item)) {
                presenter.reset();
            }
        }

        for (let i in eventData) {
            if (eventData.hasOwnProperty(i)) {
                const eventModule = eventData[i];
                if (presenter.configuration.worksWithModulesList.includes(eventModule)) {
                    presenter.sendEvent(eventName);
                    presenter.reset();
                }
            }
        }
    }

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName === "LimitedHideAnswers") {
            presenter.handleLimitedHideAnswers(eventName, eventData);
        }
        if (eventName == "HideAnswers") {
            presenter.reset();
        }
        if (eventName == "ShowAnswers") {
            presenter.$button.text(presenter.configuration.textSelected);
            presenter.$wrapper.removeClass('disabled');
            presenter.$wrapper.addClass('selected');
            presenter.configuration.isSelected = true;
            presenter.configuration.isEnabled = false;
        }
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            "show": presenter.show,
            "hide": presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getState = function () {
        return JSON.stringify({
            'isVisible': presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        presenter.setVisibility(JSON.parse(state).isVisible);
    };

    presenter.reset = function () {
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper.removeClass('selected');
        presenter.$wrapper.removeClass("disabled");
        presenter.configuration.isEnabled = true;
        presenter.configuration.isSelected = false;
    };

    presenter.setShowErrorsMode = function () {
        presenter.reset();
        presenter.$wrapper.addClass("selected");
        presenter.$wrapper.addClass("disabled");
        presenter.configuration.isEnabled = false;
    };

    presenter.setWorkMode = function () {
        presenter.reset();
    };

    presenter.getTitlePostfix = function () {
        if (presenter.configuration.isSelected) {
            return presenter.speechTexts.selected;
        } else {
            return ''
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Limited_Submit" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isLocalized="true" name="Text" nameLabel="Limited_Submit_property_text" type="string"/>
        <property isLocalized="true" name="Text selected" nameLabel="Limited_Submit_property_text_selected" type="string"/>
        <property isDefault="true" name="worksWith" nameLabel="Limited_Submit_property_works_with" type="text"/>

        <property displayName="Speech texts" name="speechTexts" nameLabel="Limited_Submit_speech_texts" type="staticlist">
            <property name="selected" nameLabel="Limited_Submit_speech_text_selected" type="staticrow">
                <property name="textToSpeechText" nameLabel="Limited_Submit_speech_text_selected" type="string"/>
            </property>

            <property name="blockEdit" nameLabel="Limited_Submit_speech_text_selected_button_edit_block" type="staticrow">
                <property name="textToSpeechText" nameLabel="Limited_Submit_speech_text_selected_button_edit_block" type="string"/>
            </property>

            <property name="noBlockEdit" nameLabel="Limited_Submit_speech_text_selected_button_no_edit_block" type="staticrow">
                <property name="textToSpeechText" nameLabel="Limited_Submit_speech_text_selected_button_no_edit_block" type="string"/>
            </property>

            <property name="notAllAttempted" nameLabel="Limited_Submit_speech_text_selected_button_not_attempted" type="staticrow">
                <property name="textToSpeechText" nameLabel="Limited_Submit_speech_text_selected_button_not_attempted" type="string"/>
            </property>
        </property>
    </model>
<css>.limited-submit-wrapper,
.limited-submit-wrapper .limited-submit-container,
.limited-submit-wrapper .limited-submit-container .limited-submit-button {
    width: 100%;
    height: 100%;
}

.limited-submit-wrapper .limited-submit-container .limited-submit-button {
    background: url('resources/submit-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}
</css><view>&lt;div class="limited-submit-wrapper"&gt;
    &lt;div class="limited-submit-container"&gt;
        &lt;div class="limited-submit-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="limited-submit-wrapper"&gt;
    &lt;div class="limited-submit-container"&gt;
        &lt;div class="limited-submit-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLimited_Submit_create() {
    var presenter = function () {
    };

    var isWCAGOn = false;

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.EVENTS_NAMES = {
        SELECTED: "selected",
        DESELECTED: "deselected",
        TRIED_SELECT: "canceled"
    };

    presenter.state = {
        isSelected: false,
        isEnabled: true,
        isVisible: true
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.sendEvent = function (eventValue) {
        var eventData = {
            'value': eventValue,
            'source': presenter.configuration.addonID
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.getWorksWithModulesList = function () {
        return presenter.configuration.worksWithModulesList.slice();    // Make a copy of this list
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.validateModel = function (model) {
        var modelValidator = new ModelValidator();

        var speechToTextListModelValidator = {
            'blockEdit': [ModelValidators.String('textToSpeechText', {default: 'Exercise edition is blocked'})],
            'noBlockEdit': [ModelValidators.String('textToSpeechText', {default: 'Exercise edition is not blocked'})],
            'notAllAttempted': [ModelValidators.String('textToSpeechText', {default: 'Not all attempted'})],
            'selected': [ModelValidators.String('textToSpeechText', {default: 'Selected'})]
        };

        var validatedModel = modelValidator.validate(model, [
            ModelValidators.utils.FieldRename("Is Visible", "isVisible", ModelValidators.Boolean("isVisible")),
            ModelValidators.utils.FieldRename("Text", "text", ModelValidators.String("text", {default: ""})),
            ModelValidators.utils.FieldRename("Text selected", "textSelected", ModelValidators.String("textSelected", {default: ""})),
            ModelValidators.utils.FieldRename("ID", "addonID", ModelValidators.DumbString("addonID")),
            ModelValidators.utils.FieldRename("Is Tabindex Enabled", "isTabindexEnabled", ModelValidators.Boolean("isTabindexEnabled")),
            ModelValidators.String("worksWith", {default: ""}),
            ModelValidators.StaticList('speechTexts', speechToTextListModelValidator)
        ]);

        if (validatedModel.isValid) {
            validatedModel.value.worksWithModulesList = validatedModel.value.worksWith.split("\n")
                .map(function (value) {
                    return value.trim();
                })
                .filter(function (value) {
                    return value !== "";
                })
                .filter(function (value, index, self) { //Unique elements
                    return self.indexOf(value) === index;
                });
        }

        return validatedModel;

    };

    presenter.handleClick = function () {
        if (presenter.state.isSelected) {
            presenter.onButtonDeselect();
        } else {
            presenter.onButtonSelect();
        }
    };

    presenter.onButtonDeselect = function () {
        var text = presenter.configuration.text;

        presenter.state.isSelected = false;
        presenter.$wrapper.removeClass('selected');

        presenter.sendEvent(presenter.EVENTS_NAMES.DESELECTED);

        presenter.executeUnCheckForAllModules();
        presenter.$button.text(text);
    };

    presenter.onButtonSelect = function () {
        if (presenter.allModulesAttempted()) {
            var text = presenter.configuration.textSelected;

            presenter.$wrapper.addClass('selected');
            presenter.state.isSelected = true;

            presenter.sendEvent(presenter.EVENTS_NAMES.SELECTED);

            presenter.executeCheckForAllModules();
            presenter.$button.text(text);
        } else {
            presenter.sendEvent(presenter.EVENTS_NAMES.TRIED_SELECT);
        }
    };


    presenter.executeCheckForAllModules = function () {
        presenter.configuration.worksWithModulesList.forEach(function (moduleId) {
            var module = presenter.playerController.getModule(moduleId);
            if (module &amp;&amp; module.setShowErrorsMode) {
                module.setShowErrorsMode();
            }
        });
    };

    presenter.executeUnCheckForAllModules = function () {
        presenter.configuration.worksWithModulesList.forEach(function (moduleId) {
            var module = presenter.playerController.getModule(moduleId);
            if (module &amp;&amp; module.setWorkMode) {
                module.setWorkMode();
            }
        });
    };

    presenter.allModulesAttempted = function () {
        var i = 0;
        var worksWithModulesList = presenter.configuration.worksWithModulesList;

        for (; i &lt; worksWithModulesList.length; i++) {
            var moduleId = worksWithModulesList[i];
            var module = presenter.playerController.getModule(moduleId);

            if (module &amp;&amp; module.isAttempted &amp;&amp; !module.isAttempted()) {
                return false;
            }
        }

        return true;
    };

    presenter.connectClickAction = function () {
        presenter.$button.on('click', function (eventData) {
            eventData.stopPropagation();
            if (presenter.state.isEnabled)
                presenter.handleClick();
        });
    };

    presenter.connectKeyDownAction = function () {
        presenter.$view.on('keydown', function (eventData) {
            if (eventData.which === 13) {
                eventData.stopPropagation();
                presenter.handleClick();
            }
        });
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.configuration = presenter.validateModel(model).value;
        presenter.$view = $(view);

        presenter.$button = presenter.$view.find('.limited-submit-button');
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper = presenter.$view.find('.limited-submit-wrapper');

        if (presenter.configuration.isTabindexEnabled) {
            presenter.$wrapper.attr('tabindex', '0');
        }

        if (!isPreview) {
            presenter.setVisibility(presenter.configuration.isVisible);
            presenter.connectClickAction();
            presenter.connectKeyDownAction();
        }
    };

    presenter.run = function (view, model) {
        presenter.view = view;
        presenter.presenterLogic(view, model, false);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        presenter.$button.off();
        presenter.$view.off();

        presenter.$button = null;
        presenter.$wrapper = null;
        presenter.$view = null;
        presenter.view = null;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            "show": presenter.show,
            "hide": presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getState = function () {
        return JSON.stringify({
            'isVisible': presenter.state.isVisible
        });
    };

    presenter.setState = function (state) {
        presenter.setVisibility(JSON.parse(state).isVisible);
    };

    presenter.reset = function () {
        presenter.resetSelection();
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.resetSelection = function () {
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper.removeClass('selected');
        presenter.$wrapper.removeClass("disabled");
        presenter.state.isEnabled = true;
        presenter.state.isSelected = false;
    };

    presenter.setShowErrorsMode = function () {
        presenter.reset();
        presenter.$wrapper.addClass("selected");
        presenter.$wrapper.addClass("disabled");
        presenter.state.isEnabled = false;
    };

    presenter.setWorkMode = function () {
        presenter.reset();
    };

    function speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    function getTextVoiceObject(text, lang) {
        return {
            text: text,
            lang: lang
        };
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.getTitlePostfix = function () {
        if(presenter.state.isSelected) {
            return presenter.configuration.speechTexts.selected.textToSpeechText;
        } else {
            return ''
        }
    };

    presenter.keyboardController = function (keycode) {
        if (keycode === 13) {
            var wasSelected = presenter.state.isSelected;
            presenter.$button.click();
            if (isWCAGOn) {
                if (presenter.state.isSelected) {
                    speak([getTextVoiceObject(presenter.configuration.speechTexts.blockEdit.textToSpeechText)]);
                } else if (wasSelected &amp;&amp; !presenter.state.isSelected) {
                    speak([getTextVoiceObject(presenter.configuration.speechTexts.noBlockEdit.textToSpeechText)]);
                } else {
                    speak([getTextVoiceObject(presenter.configuration.speechTexts.notAllAttempted.textToSpeechText)]);
                }
            }
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Line" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Rotation angle" nameLabel="Line_property_rotation_angle" type="string"/>
		<property name="Line width" nameLabel="Line_property_line_width" type="string"/>
		<property name="Line color" nameLabel="Line_property_line_color" type="string"/>
        <property name="Line opacity" nameLabel="Line_property_line_opacity" type="string"/>
		<property name="Left line ending" nameLabel="Line_property_left_line_ending" type="{None, Round, Circle, Square}"/>
        <property name="Right line ending" nameLabel="Line_property_right_line_ending" type="{None, Round, Circle, Square}"/>
	</model>
<css>.line-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    overflow: hidden;
}
</css><view>&lt;div class="line-wrapper"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="line-wrapper"&gt;

&lt;/div&gt;
</preview><presenter>function AddonLine_create() {
    var presenter = function () {
    };

    presenter.LINE_ENDING = {
        NONE: 'None',
        CIRCLE: 'Circle',
        ROUND: 'Round',
        SQUARE: 'Square'
    };

    presenter.ERROR_CODES = {
        'R01': "Rotation angle must be between 0 and 360 degrees!",
        'R02': "Rotation angle is not a number!",
        'ST1': "Line width must be a positive number",
        'ST2': "Line width is not a number!",
        'ST3': "Line color must be in RGB format (hexadecimal) and start with #",
        'ST4': "Line opacity must be a positive number between 0 and 1",
        'ST5': "Line opacity is not a number!",
        'F01': "Line color must be in RGB format (hexadecimal) and start with #",
        'RU1': "Addon dimensions are too small to draw line with endings (or line is too thick)!",
        'RU2': "Addon dimensions are too small to draw line with circle ending!",
        'RU3': "Addon dimensions are too small to draw line with square ending!"
    };

    function convertToRadians(degrees) {
        return degrees * Math.PI / 180;
    }

    function presenterLogic(view, model) {
        presenter.$view = $(view);
        var containerDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var containerDistances = DOMOperationsUtils.calculateOuterDistances(containerDimensions);

        presenter.$view.css({
            width: (presenter.$view.width() - containerDistances.horizontal) + 'px',
            height: (presenter.$view.height() - containerDistances.vertical) + 'px'
        });

        presenter.configuration = presenter.validateModel(model);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        var canvasWrapper = presenter.$view.find('.line-wrapper:first')[0];
        var wrapperDimensions = DOMOperationsUtils.getOuterDimensions(canvasWrapper);
        var wrapperDistances = DOMOperationsUtils.calculateOuterDistances(wrapperDimensions);

        var canvasWrapperWidth = presenter.$view.width() - wrapperDistances.horizontal;
        var canvasWrapperHeight = presenter.$view.height() - wrapperDistances.vertical;
        $(canvasWrapper).css({
            width: (canvasWrapperWidth &lt; 1 ? model.Width : canvasWrapperWidth) + 'px',
            height: (canvasWrapperHeight &lt; 1 ? model.Height : canvasWrapperHeight) + 'px'
        });

        var angle = parseInt(presenter.configuration.rotation);
        presenter.drawLine(canvasWrapper, canvasWrapperWidth, canvasWrapperHeight, angle);
    }

    presenter.applyStyles = function (element) {
        element.attr({
            'stroke-width': presenter.configuration.strokeWidth,
            'stroke': presenter.configuration.strokeColor,
            'opacity': presenter.configuration.strokeOpacity,
            'fill': presenter.configuration.strokeColor,
            'fill-rule': 'evenodd'
        });

        if (presenter.configuration.cornersRoundings) {
            element.attr('stroke-linecap', 'round');
        }
    };

    function transformShape(element, angle, width, height, axis) {
        // Calculate space needed for ending display
        var neededSpace = 0;
        if (angle % 90 != 0)  neededSpace = 1.7 * Math.abs(Math.cos(convertToRadians(angle))) * presenter.configuration.strokeWidth;
        //Line length after rotation
        var newLineLengh = Math.min((width - axis) / Math.abs(Math.cos(convertToRadians(angle))), (height - axis) / Math.abs(Math.cos(convertToRadians(90 - angle)))) - neededSpace;
        var roundedScale = Math.round(newLineLengh / (width) * 100) / 100; // Rounding scale to two decimal places
        var cx = parseInt(width / 2, 10);
        var cy = parseInt(height / 2, 10);

        element.transform("r" + angle + "," + cx + "," + cy);
        element.transform("...s" + roundedScale + "," + roundedScale + "," + cx + "," + cy);
    }

    function calculateSimpleLinePoints(width, height, lineWidth, lineEnding, leftLinePart) {
        //space needed for rounded endings
        var roundSpace = 0;
        if (lineEnding == presenter.LINE_ENDING.ROUND) roundSpace = lineWidth / 2;

        var tmpY = parseInt((height) / 2, 10);
        var tmpX = parseInt((width) / 2, 10);
        if (leftLinePart) { //for left part of the line
            return [
                { x: tmpX, y: tmpY },
                { x: roundSpace, y: tmpY }
            ];
        } else {
            return [
                { x: tmpX, y: tmpY },
                { x: width - roundSpace, y: tmpY }
            ];
        }
    }

    function calculateLinePoints(lineEnding, width, height, horizontalAxis, leftLinePart) {
        var points = {};
        switch (lineEnding) {
            case presenter.LINE_ENDING.CIRCLE:
                points.pointA = {
                    x: parseInt(width / 2, 10),
                    y: parseInt(height / 2, 10)
                };
                if (leftLinePart) {
                    points.pointB = {
                        x: 2 * horizontalAxis + (presenter.configuration.strokeWidth / 2) + 1.5,
                        y: parseInt(height / 2, 10)
                    };
                    points.pointC = {
                        x: 2 * horizontalAxis + (presenter.configuration.strokeWidth / 2) + 1.5,
                        y: parseInt(height / 2, 10)
                    };
                } else {
                    points.pointB = {
                        x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                        y: parseInt(height / 2, 10)
                    };
                    points.pointC = {
                        x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                        y: parseInt(height / 2, 10)
                    };
                }
                points.pointD = {
                    x: parseInt(width / 2, 10),
                    y: parseInt(height / 2, 10)
                };

                break;
            case presenter.LINE_ENDING.SQUARE:
                var widthSpace = width - (presenter.configuration.strokeWidth / 2);
                points.pointA = {
                    x: parseInt(width / 2, 10),
                    y: parseInt(height / 2, 10)
                };

                if (leftLinePart) {
                    points.pointB = {
                        x: (presenter.configuration.strokeWidth / 2) + 2 * horizontalAxis,
                        y: parseInt(height / 2, 10)
                    };
                    points.pointC = {
                        x: (presenter.configuration.strokeWidth / 2) + 2 * horizontalAxis,
                        y: parseInt((height) / 2, 10) + horizontalAxis
                    };
                    points.pointD = {
                        x: (presenter.configuration.strokeWidth / 2),
                        y: parseInt((height) / 2, 10) + horizontalAxis
                    };
                    points.pointE = {
                        x: (presenter.configuration.strokeWidth / 2),
                        y: parseInt((height) / 2, 10) - horizontalAxis
                    };
                    points.pointF = {
                        x: (presenter.configuration.strokeWidth / 2) + 2 * horizontalAxis,
                        y: parseInt((height) / 2, 10) - horizontalAxis
                    };
                    points.pointG = {
                        x: (presenter.configuration.strokeWidth / 2) + 2 * horizontalAxis,
                        y: parseInt(height / 2, 10)
                    };
                } else {
                    points.pointB = {
                        x: widthSpace - 2 * horizontalAxis,
                        y: parseInt(height / 2, 10)
                    };
                    points.pointC = {
                        x: widthSpace - 2 * horizontalAxis,
                        y: parseInt((height) / 2, 10) + horizontalAxis
                    };
                    points.pointD = {
                        x: widthSpace,
                        y: parseInt((height) / 2, 10) + horizontalAxis
                    };
                    points.pointE = {
                        x: widthSpace,
                        y: parseInt((height) / 2, 10) - horizontalAxis
                    };
                    points.pointF = {
                        x: widthSpace - 2 * horizontalAxis,
                        y: parseInt((height) / 2, 10) - horizontalAxis
                    };
                    points.pointG = {
                        x: widthSpace - 2 * horizontalAxis,
                        y: parseInt(height / 2, 10)
                    };

                }
                points.pointH = {
                    x: parseInt(width / 2, 10),
                    y: parseInt(height / 2, 10)
                };
        }
        return points;
    }

    function createLinePath(width, height, lineEnding, horizontalAxis, leftLinePart) {
        var points = [];
        var pathString;
        var verticalAxis = horizontalAxis + 0.5;
        presenter.configuration.cornersRoundings = false;
        switch (lineEnding) {
            case presenter.LINE_ENDING.NONE:
                points = calculateSimpleLinePoints(width, height, presenter.configuration.strokeWidth, lineEnding, leftLinePart);
                //fix for one round ending and second "none"
                pathString = points[0].x + "," + points[0].y;
                pathString += "L" + points[1].x + "," + points[1].y;
                pathString += "L" + points[0].x + "," + points[0].y;
                break;
            case presenter.LINE_ENDING.ROUND:
                points = calculateSimpleLinePoints(width, height, presenter.configuration.strokeWidth, lineEnding, leftLinePart);
                if (leftLinePart) {
                    pathString = points[0].x + "," + points[0].y;
                    pathString += "L" + points[1].x + "," + points[1].y;
                } else {
                    pathString = points[0].x + "," + points[0].y;
                    pathString += "L" + points[1].x + "," + points[1].y;
                }
                presenter.configuration.cornersRoundings = true;
                break;
            case presenter.LINE_ENDING.CIRCLE:
                points = calculateLinePoints(presenter.LINE_ENDING.CIRCLE, width, height, horizontalAxis, leftLinePart);
                pathString = points.pointA.x + "," + points.pointA.y + "L" + points.pointB.x + "," + points.pointB.y;
                if (leftLinePart) {
                    pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 1 " + points.pointC.x + " " + (points.pointC.y - 1);
                } else {
                    pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 0 " + points.pointC.x + " " + (points.pointC.y - 1);
                }
                pathString += "L" + points.pointC.x + "," + points.pointC.y;
                pathString += "L" + points.pointD.x + "," + points.pointD.y;
                break;
            case presenter.LINE_ENDING.SQUARE:
                points = calculateLinePoints(presenter.LINE_ENDING.SQUARE, width, height, horizontalAxis, leftLinePart);
                pathString = points.pointA.x + "," + points.pointA.y + "L" + points.pointB.x + "," + points.pointB.y;
                pathString += "L" + points.pointC.x + "," + points.pointC.y;
                pathString += "L" + points.pointD.x + "," + points.pointD.y;
                pathString += "L" + points.pointE.x + "," + points.pointE.y;
                pathString += "L" + points.pointF.x + "," + points.pointF.y;
                pathString += "L" + points.pointG.x + "," + points.pointG.y;
                pathString += "L" + points.pointH.x + "," + points.pointH.y;
                break;
        }
        return pathString;
    }


    presenter.drawLine = function (wrapper, width, height, angle) {

        if ((presenter.configuration.rightLineEnding !== presenter.LINE_ENDING.NONE) || (presenter.configuration.leftLineEnding !== presenter.LINE_ENDING.NONE)) {
            if (2 * presenter.configuration.strokeWidth + 2 &gt; height) {
                DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, 'RU1');
                return;
            }
        }

        var paper = new Raphael(wrapper, width &lt; 20 ? 20 : width, height &lt; 20 ? 20 : height);
        var radius = parseInt(presenter.configuration.strokeWidth / 2, 10) + 5;
        var horizontalAxis = radius - (presenter.configuration.strokeWidth) * 0.5 + 1.5;

        var pathString;
        pathString = "M";
        if (presenter.configuration.rightLineEnding == presenter.LINE_ENDING.ROUND) {    //for rounded ending sequence of creating path does matter
            // case for simply line with two rounded endings
            if ((presenter.configuration.leftLineEnding == presenter.LINE_ENDING.ROUND)) {
                pathString += parseInt(presenter.configuration.strokeWidth / 2, 10) + "," + parseInt(height / 2, 10);
                pathString += "L" + (width - parseInt(presenter.configuration.strokeWidth / 2, 10)) + "," + parseInt(height / 2, 10);
                presenter.configuration.cornersRoundings = true;
            }
            else {
                pathString += createLinePath(width, height, presenter.configuration.leftLineEnding, horizontalAxis, true);
                pathString += "L";
                pathString += createLinePath(width, height, presenter.configuration.rightLineEnding, horizontalAxis, false);
            }
        }
        else {
            pathString += createLinePath(width, height, presenter.configuration.rightLineEnding, horizontalAxis, false);
            pathString += "L";
            pathString += createLinePath(width, height, presenter.configuration.leftLineEnding, horizontalAxis, true);
        }

        var path = paper.path(pathString);
        presenter.applyStyles(path);

        transformShape(path, angle, width, height, horizontalAxis);
    };

    presenter.createPreview = function (view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    presenter.run = function (view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.validateModel = function (model) {

        var rotation = model["Rotation angle"];
        if (!rotation) {
            rotation = 0;
        } else {
            rotation = parseFloat(rotation);
            if (isNaN(rotation)) {
                return { isError: true, errorCode: 'R02' };
            }

            if (rotation &lt; 0 || rotation &gt; 360) {
                return { isError: true, errorCode: 'R01' };
            }
        }

        var strokeWidth = model["Line width"];
        if (!strokeWidth) {
            strokeWidth = 1;
        } else {
            strokeWidth = parseFloat(strokeWidth);
            if (isNaN(strokeWidth)) {
                return { isError: true, errorCode: 'ST2' };
            }

            if (strokeWidth &lt;= 0) {
                return { isError: true, errorCode: 'ST1' };
            }
        }

        var strokeColor = model["Line color"];
        var regExp = new RegExp("#[0-9a-fA-F]+");
        var colorMatch;

        if (!strokeColor) {
            strokeColor = "#000";
        } else {
            if (strokeColor.length &lt; 4 || strokeColor.length &gt; 7) {
                return { isError: true, errorCode: 'ST3' };
            }

            colorMatch = strokeColor.match(regExp);
            if (!colorMatch || colorMatch === null || colorMatch.length &lt; 1) {
                return { isError: true, errorCode: 'ST3' };
            }
            if (colorMatch[0].length &lt; strokeColor.length) {
                return { isError: true, errorCode: 'ST3' };
            }
        }

        var strokeOpacity = model["Line opacity"];
        if (!strokeOpacity) {
            strokeOpacity = 1;
        } else {
            strokeOpacity = parseFloat(strokeOpacity);
            if (isNaN(strokeOpacity)) {
                return { isError: true, errorCode: 'ST5' };
            }

            if (strokeOpacity &lt; 0 || strokeOpacity &gt; 1) {
                return { isError: true, errorCode: 'ST4' };
            }
        }

        var leftLineEnding = model["Left line ending"];
        if (!leftLineEnding) {
            leftLineEnding = presenter.LINE_ENDING.NONE;
        }
        var rightLineEnding = model["Right line ending"];
        if (!rightLineEnding) {
            rightLineEnding = presenter.LINE_ENDING.NONE;
        }

        var isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        return {
            isError: false,
            rotation: rotation,
            strokeWidth: strokeWidth,
            strokeColor: strokeColor,
            strokeOpacity: strokeOpacity,
            rightLineEnding: rightLineEnding,
            leftLineEnding: leftLineEnding,
            isVisibleByDefault: isVisibleByDefault,
            isVisible: isVisibleByDefault,
            cornersRoundings: false
        };
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.reset = function () {
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        var isVisible = JSON.parse(state).isVisible;

        presenter.configuration.isVisible = isVisible;
        presenter.setVisibility(isVisible);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Line_Number" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Decimal Separator" nameLabel="Line_Number_property_decimal_separator" type="string"/>
        <property name="Min" nameLabel="Line_Number_property_min" type="string"/>
        <property name="Max" nameLabel="Line_Number_property_max" type="string"/>
        <property name="Ranges" nameLabel="Line_Number_property_ranges" type="text"/>
        <property name="Not Activity" nameLabel="Line_Number_property_not_activity" type="boolean"/>
        <property name="Step" nameLabel="Line_Number_property_step" type="string"/>
        <property name="Show Axis X Values" nameLabel="Line_Number_property_show_axis_x_values" type="boolean"/>
        <property name="Axis X Values" nameLabel="Line_Number_property_axis_x_values" type="string"/>
        <property name="Disable" nameLabel="Line_Number_property_disable" type="boolean"/>
        <property name="Don't show ranges" nameLabel="Line_Number_property_dont_show_ranges" type="boolean"/>
    </model>
<css>.addon_Line_Number .outer .infinity-left,
.addon_Line_Number .outer .infinity-right {
    height: 100%;
    width: 5%;
    position: absolute;
    z-index: 9;
}

.addon_Line_Number .outer .infinity-left {
    left: 0px;
}

.addon_Line_Number .outer .infinity-right {
    right: 0px;
}

.addon_Line_Number .outer {
    width: 100%;
    height: 100%;
    background-color: #f3f3f3;
    position: relative;
}

.addon_Line_Number .inner {
    width: 90%;
    height: 100%;
    position: absolute;
    left: 5%;
    background-color: #fefefe;
}

.addon_Line_Number .x-axis {
    width: 100%;
    height: 2px;
    min-height: 2px;
    max-height: 2px;
    position: absolute;
    z-index: 12;
    top: 50%;
    background-color: #111;
}

.addon_Line_Number .stepLine {
    width: 2px;
    height: 8px;
    min-height: 8px;
    background-color: #111;
    position: absolute;
    top: 50%;
    margin-top: -4px;
}

.addon_Line_Number .rangeImage, .addon_Line_Number .currentMousePosition {
    background-repeat: no-repeat;
    width: 12px;
    height: 12px;
    position: absolute;
    left: -5px;
    top: -1px;
    z-index: 11;
    background-size: 12px 12px;
}

.addon_Line_Number .exclude {
    background-image: url('resources/range_exclude.svg');
}

.addon_Line_Number .include {
    background-image: url('resources/range_include.svg');
}

.addon_Line_Number .correctRangeExclude {
    background-image: url('resources/correct_range_exclude.svg');
}

.addon_Line_Number .correctRangeInclude {
    background-image: url('resources/correct_range_include.svg');
}

.addon_Line_Number .wrongRangeExclude {
    background-image: url('resources/wrong_range_exclude.svg');
}

.addon_Line_Number .wrongRangeInclude {
    background-image: url('resources/wrong_range_include.svg');
}

.addon_Line_Number .clickArea {
    width: 10px;
    height: 50px;
    top: -25px;
    min-height: 10px;
    min-width: 10px;
    position: absolute;
    z-index: 12;
    overflow: hidden;
}

.addon_Line_Number .stepText {
    position: absolute;
    font-size: 12px;
    top: 10px;
    white-space: nowrap;
}

.addon_Line_Number .x-arrow {
    border-bottom: 6px solid transparent;
    border-left: 6px solid black;
    border-top: 6px solid transparent;
    right: -5px;
    top: -5px;
    position: absolute;
}

.addon_Line_Number .clickArea:hover,
.addon_Line_Number .outer .infinity-left:hover,
.addon_Line_Number .outer .infinity-right:hover {
    cursor: pointer;
}

.addon_Line_Number .selectedRange {
    background-color: #00bb44;
    box-shadow: 0px 0px 1px #111;
    min-height: 10px;
    height: 10px;
    min-width: 2px;
    width: 2px;
    top: -7px;
    position: absolute;
    z-index: 10;
    border-radius: 5px 5px 0px 0px;
}

.addon_Line_Number .currentSelectedRange {
    background-color: #00aaff;
}

.addon_Line_Number .correct {
    background-color: #00ff44;
}

.addon_Line_Number .wrong {
    background-color: #ff3344;
}

.addon_Line_Number .currentMousePosition {
    background-image: url('resources/current_mouse_pos.svg');
}

.addon_Line_Number .infinityLeft {
    border-radius: 0px 5px 0px 0px;
}

.addon_Line_Number .infinityRight {
    border-radius: 5px 0px 0px 0px;
}

.addon_Line_Number .infinityBoth {
    border-radius: 0px;
}

.addon_Line_Number .infinity-hover {
    background-color: #d8d8d8;
}
</css><view>&lt;div class="outer"&gt;
    &lt;div class="infinity-left" value="-INF"&gt;&lt;/div&gt;
        &lt;div class="inner"&gt;
            &lt;div class="x-axis"&gt;
                &lt;div class="x-arrow"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;div class="infinity-right" value="INF"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="outer"&gt;
    &lt;div class="infinity-left" value="-INF"&gt;&lt;/div&gt;
    &lt;div class="inner"&gt;
        &lt;div class="x-axis"&gt;
            &lt;div class="x-arrow"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="infinity-right" value="INF"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonLine_Number_create() {
    /*
        KNOWN ISSUES:
            PROPERTIES:
                Axis X Values:
                    0* - due to backward compatybility it should be treaten as 1*

            (04.13.2015) CLICK &amp; DRAWING RANGES LOGIC:
                Due to logic of drawing ranges, presenter.configuration.max should be set before creating steps as a max
                value from field values. Changing this logic will break user click logic, which will not draw range to
                infinity right.


     */

    var presenter = function() {};

    var eventBus,
        playerController;

    presenter.configuration = {};
    presenter.checkedPoints = [];
    presenter.results = {
        correct: 0,
        wrong: 0
    };
    presenter.singleDot = {
        value: -1,
        element: null
    };
    presenter.isGradualShowAnswersActive = false;
    presenter.currentRanges = null;
    presenter.isFirstRun = true;
    presenter.isProcessing = false;

    presenter.maxElement = function (array) {
        if (array.length == 0) {
            throw "Empty array";
        }

        return Math.max.apply(null, array);
    };

    presenter.errorCodes = {
        'MIN01' : 'Min value cannot be empty.',
        'MIN02' : 'Min value must be a number.',
        'MIN03' : 'Min value does not fit the separator.',
        'MAX01' : 'Max value cannot be empty.',
        'MAX02' : 'Max value must be a number',
        'MAX03' : 'Max value does not fit the separator.',
        'MAX04' : 'Max value must be within xAxisValues. Suggested value: {{lastValue}} or {{lastValuePlusStep}}.',
        'MIN/MAX01' : 'Min value cannot be greater than Max value.',
        'RAN01' : 'One or more ranges are invalid.',
        'RAN02' : 'One or more ranges are invalid. Please make sure, that all ranges start/end can be displayed on X axis.',
        'STEP01' : 'The value in Step property is invalid.',
        'STEP02' : 'The value in Step does not fit the separator.',
        'STEP03' : 'The value in step property have to be greater than 0',
        'VAL01' : 'One or more X axis values are invalid.',
        'VAL02' : 'One or more X axis do not fit the separator.',
        'OOR01' : 'Can not resolve which range is currently selected.',
        'DSE01' : 'Semicolon is a reserved symbol.',
        'AXV_01': "Axis X cyclic values have to be greater or equal than 0.",
        'AXV_02': "Axis X fixed values have to be greater or equal than Min.",
        'AXV_03': "Axis X fixed values have to be lower or equal than Max.",
        'AXV_04': "Axis X Values property can have only number values.",
        'AXV_05': "Axis X Valuese property cant have duplicates."
    };

    presenter.CLICKED_POSITION = {
        START: 1,
        MIDDLE: 2,
        END: 3,
        NONE: 4
    };

    presenter.run = function(view, model) {
        const events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        presenter.presenterLogic(view, model, false);

        const _self = this;
        events.forEach((event) =&gt; {
            eventBus.addEventListener(event, _self);
        });
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.removeZIndexes = function () {
        var selector = '.outer .infinity-left, .outer .infinity-right, .x-axis, .rangeImage, .addon_Line_Number .currentMousePosition, .clickArea, .selectedRange';
        presenter.$view.find(selector).css('z-index', '0');
    };

    function addInitPoints () {
        var points = presenter.configuration.shouldDrawRanges;
        for (var i = 0; i &lt; points.length; i++) {
            if (points[i].start.value === points[i].end.value) {
                presenter.checkedPoints.push(points[i].values[0]);
            }
        }
    }

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.$view.disableSelection();
        presenter.configuration = presenter.validateModel(model);

        presenter.configuration.isPreview = isPreview;

        if ( presenter.configuration.isError ) {
            return DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.errorCodes, presenter.configuration.errorCode);
        }

        presenter.createSteps();

        if ( !isPreview &amp;&amp; !presenter.configuration.isDisabled &amp;&amp; !presenter.configuration.dontShowRanges) {
            presenter.bindInfinityAreas();
        }

        presenter.drawRanges(presenter.configuration.shouldDrawRanges, true);

        presenter.configuration.isInitialDraw = false;

        if (presenter.configuration.dontShowRanges) {
            addInitPoints();
        }

        if ( !presenter.configuration.isVisibleByDefault &amp;&amp; !isPreview) {
            presenter.hide();
        }

        if ( isPreview ) {
            // z-index in Editor breaks down properties popups
            presenter.removeZIndexes();
        }
    };

    presenter.bindInfinityAreas = function() {
        var infinityLeft = presenter.$view.find('.infinity-left');
        var infinityRight = presenter.$view.find('.infinity-right');

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            infinityLeft.on('touchstart', function (e) {
                e.stopPropagation();
                e.preventDefault();

                presenter.configuration.touchData.lastEvent = e;
            });

            infinityLeft.on('touchend', function (e) {
                e.stopPropagation();
                e.preventDefault();

                if (presenter.configuration.touchData.lastEvent.type != e.type) {
                    var eventData = event.touches[0] || event.changedTouches[0];
                    clickLogic(eventData.target);
                }

            });
        }
        else {
            infinityLeft.on('click', function (e) {
                e.stopPropagation();
                e.preventDefault();
                clickLogic($(e.target));
            });
        }

        infinityLeft.hover(function() {
            infinityLeft.addClass('infinity-hover');
        }, function() {
            infinityLeft.removeClass('infinity-hover');
        });

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            infinityRight.on('touchstart', function (e) {
                e.stopPropagation();
                e.preventDefault();
                presenter.configuration.touchData.lastEvent = e;
            });

            infinityRight.on('touchend', function (e) {
                e.stopPropagation();
                e.preventDefault();

                if (presenter.configuration.touchData.lastEvent.type != e.type) {
                    var eventData = event.touches[0] || event.changedTouches[0];
                    clickLogic(eventData.target);
                }
            });
        }
        else {
            infinityRight.on('click', function (e) {
                e.stopPropagation();
                e.preventDefault();
                clickLogic($(e.target));
            });
        }

        infinityRight.hover(function () {
            infinityRight.addClass('infinity-hover');
        }, function () {
            infinityRight.removeClass('infinity-hover');
        });
    };

    function calculateStepWidth(xAxisValues) {
        var xAxisWidth = presenter.$view.find('.x-axis').width();
        return xAxisWidth / (xAxisValues.length + 1);
    }

    function setClickedRanges(e) {
        var ranges = presenter.configuration.drawnRangesData.ranges,
            value = parseRangeStartOrEnd($(e).attr('value'), presenter.configuration.separator);

        presenter.configuration.mouseData.clickedRanges = [];

        $.each(ranges, function() {
            if ( $.inArray(value, this.values) &gt;= 0  ) {
                presenter.configuration.mouseData.clickedRanges.push(this);
            }
        });

    }

    function getClickedRangePosition(e) {
        var range = presenter.configuration.mouseData.clickedRanges[0];

        if ( $(e).attr('value') == '-INF' ) {
            return presenter.CLICKED_POSITION.START;
        } else if ( $(e).attr('value') == 'INF' ) {
            return presenter.CLICKED_POSITION.END;
        } else if (range.start.element[0] == $(e).parent()[0]) {
            return presenter.CLICKED_POSITION.START;
        } else if (range.end.element[0] == $(e).parent()[0]) {
            return presenter.CLICKED_POSITION.END;
        } else {
            return presenter.CLICKED_POSITION.MIDDLE;
        }
    }

    presenter.removeRange = function(range, removeIncludeImages) {
        getSelectedRange(range).remove();
        if (!range.values) { range.values = [] }

        var index = presenter.configuration.drawnRangesData.ranges.indexOf(range);

        if (index &gt;= 0) {
            var removed = presenter.configuration.drawnRangesData.ranges.splice(index, 1);

            if (removeIncludeImages &amp;&amp; removed.length &gt; 0) {
                var start = parseElement(removed[0].start.element);
                var end = parseElement(removed[0].end.element);
                $(start).find('.rangeImage').remove();
                $(end).find('.rangeImage').remove();
                $(start).find('.selectedRange').remove();
            }
        }

        $.each(range.values, function() {
            var value = parseRangeStartOrEnd(this, presenter.configuration.separator);
            var index = presenter.configuration.drawnRangesData.values.indexOf(value);
            presenter.configuration.drawnRangesData.values.splice(index, 1);
        });

    };

    function splitRange(range, e) {

        presenter.removeRange(range, false);
        var clickedArea = $(e);

        var firstRange = {
            'start' : range.start,
            'end' : createRangeElement(clickedArea, clickedArea.attr('value'), false)
        };

        var secondRange = {
            'start' : firstRange.end,
            'end' : range.end
        };

        presenter.drawRanges([firstRange, secondRange], true);
    }

    function joinRanges(ranges) {
        var firstRange, secondRange;
        var min = 1000,
            max = -1000;

        $.each(ranges, function() {
            if (this.end.value &gt; max) {
                max = this.end.value;
                secondRange = this;
            }

            if (this.start.value &lt; min) {
                min = this.start.value;
                firstRange = this;
            }
        });

        $.each(ranges, function() {
            presenter.removeRange(this, true);
        });

        var joinedRange = {
            'start' : firstRange.start,
            'end' : secondRange.end
        };

        removeRangesBetweenRange(joinedRange);

        presenter.drawRanges([joinedRange], true);

    }

    function setClicks(e) {

        var element = $(e);
        var position;

        if ( presenter.isMouseAboveExistingRange(e) ) {
            position = getClickedRangePosition(e);
        } else {
            position = presenter.CLICKED_POSITION.NONE;
        }

        var click = {
            element: element,
            position: position,
            time: (new Date()).getTime()
        };

        presenter.configuration.mouseData.clicks.push( click );
    }

    function displayCurrentMousePosition(e) {
        presenter.$view.find('.currentMousePosition').remove();
        var circle = $('&lt;div&gt;&lt;/div&gt;');
        circle.addClass('currentMousePosition');
        $(e).parent().append(circle);
    }

    function hideCurrentMousePosition() {
        presenter.$view.find('.currentMousePosition').remove();
    }

    function createClickArea(element, value) {
        var clickArea = $('&lt;div&gt;&lt;/div&gt;');
        var selectedRange = $('&lt;div&gt;&lt;/div&gt;');

        clickArea.addClass('clickArea');
        selectedRange.addClass('selectedRange');

        $(element).append(clickArea);
        clickArea.attr('value', value);

        var width = presenter.configuration.stepWidth, left = - (presenter.configuration.stepWidth / 2) + 'px';

        if ( value == presenter.configuration.min || value == presenter.configuration.max ) {
            width = width / 2;
        }

        if ( value == presenter.configuration.min ) {
            left = 0;
        }

        if ( value == presenter.configuration.max ) {
            left = - width;
        }

        clickArea.css({
            'width' : width,
            'left' : left
        });

    }

    function bindClickAreaListeners(clickArea) {
        removeAllClickListeners();

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            clickArea.on('touchstart', function (e) {
                e.stopPropagation();
                e.preventDefault();

                presenter.configuration.touchData.lastEvent = e;
            });

            clickArea.on('touchend', function (e) {
                e.stopPropagation();
                e.preventDefault();

                if ( presenter.configuration.touchData.lastEvent.type != e.type ) {
                    var eventData = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];
                    if (presenter.configuration.dontShowRanges) {
                        togglePoint($(eventData.target));
                    } else {
                        clickLogic($(eventData.target));
                    }
                }
            });

            clickArea.on('click', function (e) {
                e.stopPropagation();
            });
        }
        else {
            clickArea.on('mouseleave', function (e) {
                e.stopPropagation();
                hideCurrentMousePosition();
            });

            clickArea.on('mouseenter', function (e) {
                e.stopPropagation();
                e.preventDefault();
                displayCurrentMousePosition($(e.target));
            });

            clickArea.on('contextmenu', function (e) {
                e.stopPropagation();
                e.preventDefault();
            });

            clickArea.on('click', function (e) {
                e.stopPropagation();
                e.preventDefault();
                if (presenter.configuration.dontShowRanges) {
                    togglePoint($(e.target));
                } else {
                    clickLogic($(e.target));
                }
            });
        }
    }

    function removeAllClickListeners() {
        var clickArea = presenter.$view.find('.clickArea');
        var infinityLeft = presenter.$view.find('.infinity-left');
        var infinityRight = presenter.$view.find('.infinity-right');
        var listeners = 'mouseleave mouseenter contextmenu touchstart touchend click';

        clickArea.off(listeners);
        infinityRight.off(listeners);
        infinityLeft.off(listeners);
    }

    function isFirstClick() {
        return presenter.configuration.mouseData.clicks.length == 1;
    }

    function isSecondClick() {
        return presenter.configuration.mouseData.clicks.length == 2;
    }

    function areBothClicksNone() {
        return presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.NONE
            &amp;&amp; presenter.configuration.mouseData.clicks[1].position == presenter.CLICKED_POSITION.NONE;
    }

    function isFirstClickNoneAndSecondNotNone() {
        return presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.NONE
            &amp;&amp; presenter.configuration.mouseData.clicks[1].position != presenter.CLICKED_POSITION.NONE;
    }

    function areTwoClickedRanges() {
        return presenter.configuration.mouseData.clickedRanges.length == 2;
    }

    function isClickedStartOrEnd() {
        return presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.END
            || presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.START;
    }

    function isClickedMiddle() {
        return presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.MIDDLE;
    }

    function isFirstStartOrEndAndSecondMiddleClicked() {
        return presenter.configuration.mouseData.clicks[1].position == presenter.CLICKED_POSITION.MIDDLE
            &amp;&amp; isClickedStartOrEnd()
            &amp;&amp; isBothClicksTheSameRange()
    }

    function isFirstStartOrEndAndSecondNoneClicked() {
        return presenter.configuration.mouseData.clicks[1].position == presenter.CLICKED_POSITION.NONE
            &amp;&amp; isClickedStartOrEnd();
    }

    // toggle include image when this situation happens
    function isTheSameRangeEndOrStartClickedInBothClicks() {
        return presenter.configuration.mouseData.clicks[0].position == presenter.configuration.mouseData.clicks[1].position
            &amp;&amp; presenter.configuration.mouseData.clicks[0].element[0] == presenter.configuration.mouseData.clicks[1].element[0];
    }

    // this is when range should be deleted when both clicks were on the same range or joined when 1st/2nd click is on different range
    function isBothClicksTheSameRangeStartOrEnd() {
        return presenter.configuration.mouseData.clicks[0].element[0] != presenter.configuration.mouseData.clicks[1].element[0]
            &amp;&amp; (presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.END
            || presenter.configuration.mouseData.clicks[0].position == presenter.CLICKED_POSITION.START)
            &amp;&amp; (presenter.configuration.mouseData.clicks[1].position == presenter.CLICKED_POSITION.END
            || presenter.configuration.mouseData.clicks[1].position == presenter.CLICKED_POSITION.START);
    }

    function isBothClicksTheSameRange() {
        var firstClickRange = getRangeByValue( presenter.configuration.mouseData.clicks[0].element.attr('value') );
        var secondClickRange = getRangeByValue( presenter.configuration.mouseData.clicks[1].element.attr('value') );
        return compareRanges(firstClickRange, secondClickRange);
    }

    function setCurrentClickedRange() {
        var first, second;

        if ( presenter.configuration.mouseData.twoClickedRangesCount == 1 ) {

            if ( presenter.configuration.mouseData.clickedRanges[0].start.value &gt; presenter.configuration.mouseData.clickedRanges[1].start.value ) {
                first = presenter.configuration.mouseData.clickedRanges[1];
                second = presenter.configuration.mouseData.clickedRanges[0];
            } else {
                first = presenter.configuration.mouseData.clickedRanges[0];
                second = presenter.configuration.mouseData.clickedRanges[1];
            }
        } else {
            if ( presenter.configuration.mouseData.clickedRanges[0].start.value &gt; presenter.configuration.mouseData.clickedRanges[1].start.value ) {
                first = presenter.configuration.mouseData.clickedRanges[0];
                second = presenter.configuration.mouseData.clickedRanges[1];
            } else {
                first = presenter.configuration.mouseData.clickedRanges[1];
                second = presenter.configuration.mouseData.clickedRanges[0];
            }
        }

        presenter.configuration.mouseData.clickedRanges = [first, second];
    }

    function resetClicks() {

        if ( presenter.configuration.notCurrentSelectedRange ) {
            if ( presenter.configuration.mouseData.twoClickedRangesCount == 1 ) {
                addEndRangeImage( presenter.configuration.notCurrentSelectedRange.start.element, false );
            } else if ( presenter.configuration.mouseData.twoClickedRangesCount == 2 ) {
                addEndRangeImage( presenter.configuration.notCurrentSelectedRange.end.element, false );
            }
        }

        presenter.$view.find('.currentSelectedRange').removeClass('currentSelectedRange');

        presenter.configuration.mouseData.clicks = [];
        presenter.configuration.mouseData.twoClickedRangesCount = 0;
        presenter.configuration.notCurrentSelectedRange = null;
    }

    function getSelectedRange(range) {
        var selectedRange;

        if ( isValueInfinity(range.start.value) ) {
            selectedRange = presenter.$view.find('.clickArea[value="' + presenter.configuration.min + '"]').parent().find('.selectedRange');
        } else {
            selectedRange = range.start.element.find('.selectedRange');
        }

        return selectedRange;
    }

    function isLineNumberDisabled () {
        return ((presenter.configuration.isActivity &amp;&amp; presenter.configuration.isShowErrorsMode) ||
                 presenter.configuration.isDisabled);
    }

    function isPointCorrect(point) {
        var correctPoints = getCorrectPoints();

        if (correctPoints.indexOf(point) !== -1) {
            return true;
        }
    }

    function togglePoint($eventTarget) {
        if (presenter.configuration.isShowErrorsMode || isLineNumberDisabled() || presenter.isAnswersDisplayed()) {
            return;
        }
        var $parent = $eventTarget.parent('.stepLine');
        var $rangeImage = $parent.find('.rangeImage');

        if ($rangeImage.length === 1) {
            $rangeImage.remove();

            presenter.checkedPoints = presenter.checkedPoints.filter(function(elem) {
                return elem != $eventTarget[0].value;
            });

            eventBus.sendEvent("ValueChanged", presenter.createActionEventData($eventTarget[0].value, "0", isPointCorrect($eventTarget[0].value) ? 1 : 0));
        } else {
            var $imageContainer = $('&lt;div&gt;&lt;/div&gt;');

            $imageContainer.addClass('rangeImage exclude include');
            $parent.append($imageContainer);

            presenter.checkedPoints.push($eventTarget[0].value);

            eventBus.sendEvent("ValueChanged", presenter.createActionEventData($eventTarget[0].value, "1", isPointCorrect($eventTarget[0].value) ? 1 : 0));
        }

        var correctPoints = getCorrectPoints().sort().toString(),
            checkedPoints = $.extend(true, [], presenter.checkedPoints);

            checkedPoints = checkedPoints.sort().toString();

        if (correctPoints === checkedPoints) {
            eventBus.sendEvent("ValueChanged", presenter.createAllOKEventData());
        }

    }

    function clickLogic(eventTarget) {
        if (isLineNumberDisabled() || presenter.isAnswersDisplayed() || presenter.configuration.dontShowRanges) {
            return;
        }

        if (presenter.configuration.mouseData.twoClickedRangesCount &gt; 3) {
            presenter.configuration.mouseData.twoClickedRangesCount = 0;
        }

        setClickedRanges(eventTarget);
        setClicks(eventTarget);

        var firstClick = presenter.configuration.mouseData.clicks[0];

        if (isFirstClick()) {
            if (firstClick.position == presenter.CLICKED_POSITION.NONE) {
                var value = parseRangeStartOrEnd(presenter.configuration.mouseData.clicks[0].element.attr('value'),
                                                 presenter.configuration.separator);
                presenter.singleDot = {
                    value: value,
                    element: firstClick.element
                };

                if (!isValueInfinity(value)) {
                    addEndRangeImage(presenter.configuration.mouseData.clicks[0].element.parent(), true);
                }
            } else if (areTwoClickedRanges()) {
                presenter.configuration.mouseData.twoClickedRangesCount++;

                if (presenter.configuration.mouseData.twoClickedRangesCount == 1) {
                    var selectedRange = getSelectedRange(presenter.configuration.mouseData.clickedRanges[0]);
                    selectedRange.addClass('currentSelectedRange');
                }

                presenter.configuration.notCurrentSelectedRange = presenter.configuration.mouseData.clickedRanges[1];
            } else if (isClickedStartOrEnd()) {
                var clickedRange = presenter.configuration.mouseData.clickedRanges[0],
                    selectedRange = getSelectedRange(clickedRange);

                selectedRange.addClass('currentSelectedRange');
            } else if (isClickedMiddle()) {
                splitRange(presenter.configuration.mouseData.clickedRanges[0], eventTarget);
                resetClicks();
            }
        } else if (isSecondClick()) {
            presenter.singleDot = {
                value: -1,
                element: null
            };
            var secondClick = presenter.configuration.mouseData.clicks[1];
            var firstValue = parseRangeStartOrEnd(firstClick.element.attr('value'), presenter.configuration.separator);
            var secondValue = parseRangeStartOrEnd(secondClick.element.attr('value'), presenter.configuration.separator);
            var newRange;
            var isInf = firstValue.toString().toLowerCase().includes('infinity');
            var areClickEqualAndInf = firstValue === secondValue &amp;&amp; isInf;

            if (areBothClicksNone()) {
                var timeDiff = presenter.configuration.mouseData.clicks[1].time - presenter.configuration.mouseData.clicks[0].time;
                if (timeDiff &lt; 250 &amp;&amp; timeDiff &gt; 0 &amp;&amp; !areClickEqualAndInf) {
                    newRange = {
                        start: createRangeElement(firstClick.element, firstValue, true),
                        end: createRangeElement(firstClick.element, firstValue, true)
                    };
                    setRangeValues(newRange, true);
                    addToDrawnRanges(newRange);
                } else {
                    if (firstValue &gt; secondValue) {
                        newRange = {
                            start: createRangeElement(secondClick.element, secondValue, true),
                            end: createRangeElement(firstClick.element, firstValue, true)
                        };
                        removeRangesBetweenRange(newRange);
                        presenter.drawRanges([newRange], true);
                    } else if (firstValue &lt; secondValue) {
                        newRange = {
                            start: createRangeElement(firstClick.element, firstValue, true),
                            end: createRangeElement(secondClick.element, secondValue, true)
                        };
                        removeRangesBetweenRange(newRange);
                        presenter.drawRanges([newRange], true);
                    } else {
                        var rangeImage = firstClick.element.parent().find('.rangeImage');

                        if ( rangeImage.hasClass('include') &amp;&amp; !areClickEqualAndInf) {
                            rangeImage.remove();
                        } else {
                            rangeImage.removeClass('exclude');
                            rangeImage.addClass('include');
                        }
                    }
                }
                resetClicks();

            } else if (isFirstClickNoneAndSecondNotNone()) {
                if (firstValue &gt;  secondValue) {
                    newRange = {
                        start: createRangeElement(secondClick.element, secondValue, true),
                        end: createRangeElement(firstClick.element, firstValue, true)
                    };
                } else if (firstValue &lt; secondValue) {
                    newRange = {
                        start: createRangeElement(firstClick.element, firstValue, true),
                        end: createRangeElement(secondClick.element, secondValue, true)
                    };
                }
                joinRanges([newRange, presenter.configuration.mouseData.clickedRanges[0]]);
                resetClicks();

            } else if (isFirstStartOrEndAndSecondMiddleClicked()) {
                var clickedRange = presenter.configuration.mouseData.clickedRanges[0];

                if (firstClick.position == presenter.CLICKED_POSITION.START) {
                    newRange = {
                        start: createRangeElement( secondClick.element, secondValue, firstClick.element.parent().find('.rangeImage').hasClass('include') ),
                        end: createRangeElement( clickedRange.end.element, clickedRange.end.value, clickedRange.end.include )
                    };
                } else if ( firstClick.position == presenter.CLICKED_POSITION.END ) {
                    newRange = {
                        end: createRangeElement( secondClick.element, secondValue, firstClick.element.parent().find('.rangeImage').hasClass('include') ),
                        start: createRangeElement( clickedRange.start.element, clickedRange.start.value, clickedRange.start.include )
                    };
                }
                presenter.removeRange(clickedRange, true);
                presenter.drawRanges([newRange], true);
                resetClicks();

            } else if (areTwoClickedRanges()) {
                presenter.configuration.mouseData.twoClickedRangesCount++;

                if (presenter.configuration.mouseData.twoClickedRangesCount == 2) {
                    var currentSelectedRange = getSelectedRange(presenter.configuration.mouseData.clickedRanges[0]);
                    var selectedRange = getSelectedRange(presenter.configuration.mouseData.clickedRanges[1]);
                    selectedRange.addClass('currentSelectedRange');
                    currentSelectedRange.removeClass('currentSelectedRange');
                    presenter.configuration.mouseData.clicks = presenter.configuration.mouseData.clicks.slice(0, 1);
                    presenter.configuration.mouseData.clicks[0].position = presenter.CLICKED_POSITION.START;
                    presenter.configuration.notCurrentSelectedRange = presenter.configuration.mouseData.clickedRanges[0];
                } else if (presenter.configuration.mouseData.twoClickedRangesCount == 3) {
                    joinRanges( presenter.configuration.mouseData.clickedRanges );
                    resetClicks();
                } else {
                    presenter.removeRange(getRangeByValue( firstValue ), true);
                    addEndRangeImage(secondClick.element.parent(), false);
                    resetClicks();
                }

                setCurrentClickedRange();

            } else if ( isFirstStartOrEndAndSecondNoneClicked() ) {
                if ( firstValue &gt; secondValue ) {
                    newRange = {
                        start: createRangeElement(secondClick.element, secondValue, firstClick.element.parent().find('.rangeImage').hasClass('include')),
                        end: createRangeElement(firstClick.element, firstValue, firstClick.element.parent().find('.rangeImage').hasClass('include'))
                    };

                    joinRanges( [newRange, getRangeByValue( firstValue )] );
                } else if ( firstValue &lt; secondValue ) {
                    newRange = {
                        start: createRangeElement(firstClick.element, firstValue, firstClick.element.parent().find('.rangeImage').hasClass('include') ),
                        end: createRangeElement(secondClick.element, secondValue, firstClick.element.parent().find('.rangeImage').hasClass('include') )
                    };

                    joinRanges( [newRange, getRangeByValue( firstValue )] );
                }

                resetClicks();

            } else if ( isTheSameRangeEndOrStartClickedInBothClicks() ) {
                var imageWrapper = firstClick.element.parent().find('.rangeImage');
                var shouldInclude = !imageWrapper.hasClass('include');
                var index = presenter.configuration.drawnRangesData.ranges.indexOf(presenter.configuration.mouseData.clickedRanges[0]);
                var isInf = firstValue.toString().toLowerCase().includes('infinity');
                if ( presenter.configuration.mouseData.clicks[0].position === presenter.CLICKED_POSITION.START &amp;&amp; !isInf) {
                    presenter.configuration.drawnRangesData.ranges[index].start.include = shouldInclude;
                } else if ( presenter.configuration.mouseData.clicks[0].position === presenter.CLICKED_POSITION.END &amp;&amp; !isInf) {
                    presenter.configuration.drawnRangesData.ranges[index].end.include = shouldInclude;
                }

                if (!(firstClick.element.hasClass('infinity-right') || firstClick.element.hasClass('infinity-left'))) {
                    toggleIncludeImage( imageWrapper, shouldInclude );
                }

                if ( presenter.configuration.drawnRangesData.ranges[index].values.length === 1 ) {
                    presenter.removeRange( presenter.configuration.drawnRangesData.ranges[index], true );
                    imageWrapper.remove();
                }

                presenter.$view.find('.currentSelectedRange').removeClass('currentSelectedRange');
                if (!(presenter.configuration.drawnRangesData.ranges[index] === undefined) &amp;&amp; !presenter.isAnswersDisplayed()) {
                    var rangeString = presenter.convertRangeToString(presenter.configuration.drawnRangesData.ranges[index]);
                    var eventData = presenter.createEventData(rangeString, false, checkIsRangeCorrect(presenter.configuration.drawnRangesData.ranges[index]));

                    eventBus.sendEvent('ValueChanged', eventData);
                }

                if ( presenter.allRangesCorrect() &amp;&amp; !presenter.isAnswersDisplayed()) {
                    var eventData = presenter.createAllOKEventData();
                    eventBus.sendEvent('ValueChanged', eventData);
                }

                resetClicks();

            } else if (isBothClicksTheSameRangeStartOrEnd()) {
                var firstClickRange = getRangeByValue( firstValue );
                var secondClickRange = getRangeByValue( secondValue );

                if ( compareRanges(firstClickRange, secondClickRange)) {
                    presenter.removeRange(firstClickRange, true);

                    var rangeString = presenter.convertRangeToString(firstClickRange);
                    var eventData = presenter.createEventData(rangeString, true, !checkIsRangeCorrect(firstClickRange));
                    eventBus.sendEvent('ValueChanged', eventData);

                    if ( presenter.allRangesCorrect() ) {
                        var eventData = presenter.createAllOKEventData();
                        eventBus.sendEvent('ValueChanged', eventData);
                    }
                } else {
                    joinRanges([ getRangeByValue( firstValue ), getRangeByValue( secondValue ) ]);
                }
                resetClicks();

            } else {
                resetClicks();
            }
        }
    }

    function removeRangesBetweenRange(range) {
        var currentRanges = [];
        currentRanges = currentRanges.concat(presenter.configuration.drawnRangesData.ranges);

        for( var i = 0; i &lt; currentRanges.length; i++) {
            var drawnRange = currentRanges[i];
            var drawnStartValue = drawnRange.start.value;
            var drawnEndValue = drawnRange.end.value;

            if ( drawnStartValue == -Infinity ) {
                drawnStartValue = presenter.configuration.min;
            }

            if ( drawnEndValue == Infinity ) {
                drawnEndValue = presenter.configuration.max;
            }

            if ( range.start.value &lt; drawnEndValue &amp;&amp; range.end.value &gt; drawnStartValue ) {
                presenter.removeRange(drawnRange, true);
            }
        }

    }

    function getRangeByValue(value) {
        value = parseRangeStartOrEnd(value, presenter.configuration.separator);
        var ranges = [];

        $.each(presenter.configuration.drawnRangesData.ranges, function() {
            if ( this.values.indexOf(value) &gt;= 0 ) {
                ranges.push(this);
            }

            return true;
        });

        if (ranges.length == 2) {

            if (presenter.configuration.mouseData.twoClickedRangesCount == 1) {
                return ranges[0];
            } else if (presenter.configuration.mouseData.twoClickedRangesCount == 2) {
                return ranges[1];
            }

        } else if (ranges.length == 1) {
            return ranges[0];
        } else {

            presenter.configuration.isError = true;
            presenter.configuration.errorCode = 'OOR01';
            return null;

        }
    }

    function createRangeElement(element, value, include) {
        if (element.hasClass('clickArea')) {
            element = element.parent();
        }
        return {
            element: parseElement(element),
            value: parseRangeStartOrEnd(value, presenter.configuration.separator),
            include: include
        }
    }

    presenter.isMouseAboveExistingRange = function(e) {
        var value = parseRangeStartOrEnd($(e).attr('value'), presenter.configuration.separator);
        return $.inArray( value, presenter.configuration.drawnRangesData.values ) &gt;= 0;
    };

    presenter.isValueInRange = function(value, range, takeExcludeIntoConsideration) {

        var start, end;
        if (takeExcludeIntoConsideration) {
            start = range.start.include ? range.start.value : range.start.value + 1;
            end = range.end.include ? range.end.value + 1 : range.end.value;
        } else {
            start = range.start.value;
            end = range.end.value + 1;
        }

        for( var i = start; i &lt; end; i++ ) {
            if ( i == value ) {
                return true;
            }
        }
        return false;
    };

    function toggleIncludeImage(imageWrapper, shouldInclude) {
        if (shouldInclude) {
            imageWrapper.addClass('include');
            imageWrapper.removeClass('exclude');
        } else {
            imageWrapper.addClass('exclude');
            imageWrapper.removeClass('include');
        }
    }

    function getStartElement(isStartInfinity, startValue) {
        var startElement;

        if ( isStartInfinity ) {
            startElement = presenter.$view.find('.clickArea[value="' + presenter.configuration.min + '"]').parent();
        } else {
            startElement = presenter.$view.find('.clickArea[value="' + startValue + '"]').parent();
        }

        return startElement;
    }

    function getEndElement(isEndInfinity, endValue) {
        var endElement;

        if ( isEndInfinity ) {
            endElement = presenter.$view.find('.clickArea[value="' + presenter.configuration.max + '"]').parent();
        } else {
            endElement = presenter.$view.find('.clickArea[value="' + endValue + '"]').parent();
        }

        return endElement;
    }

    presenter.drawRanges = function(ranges, shouldSendEvents) {

        $.each(ranges, function() {

            var startValue = Math.min(this.start.value, this.end.value);
            var endValue = Math.max(this.start.value, this.end.value);

            var isEndInfinity = isValueInfinity(endValue);
            var isStartInfinity = isValueInfinity(startValue);
            var startElement = getStartElement(isStartInfinity, startValue);
            var endElement = getEndElement(isEndInfinity, endValue);

            if (!this.start.element || !this.end.element) {
                this.start.element = startElement;
                this.end.element = endElement;
            }

            if ( startValue == endValue ) {
                setRangeValues(this, true);
                addToDrawnRanges(this, shouldSendEvents);
                addEndRangeImage(endElement, true);

                // if start and end values are the same, that means range is a single point, so it should not draw range

                return true;
            }

            var start = parseFloat($(startElement).css('left'));
            var end = parseFloat(endElement.css('left'));
            var difference =  Math.abs(start - end);
            var range = $('&lt;div&gt;&lt;/div&gt;');

            range.addClass('selectedRange');

            addInfinityClass(isStartInfinity, isEndInfinity, range);

            // when range is ending in infinity then it should be wider because there is space between arrowhead and last step line
            // + 2 is because stepLine is 2 px width
            var width = calculateRangeWidth(isEndInfinity, isStartInfinity, difference);

            range.css('width', width);
            startElement.append(range);

            if (start &gt; end) {
                range.css('left', - (difference) + 'px');
            }

            if (isStartInfinity) {
                range.css('left', -presenter.configuration.stepWidth + 'px');
            }

            addToDrawnRanges(this, shouldSendEvents);
            setRangeValues(this, true);
            addEndRangeImages(this, startElement, endElement, isStartInfinity, isEndInfinity);
        });

    };

    function addEndRangeImages(range, startElement, endElement, isStartInfinity, isEndInfinity) {
        if (!isEndInfinity) {
            addEndRangeImage(endElement, range.end.include);
        }

        if (!isStartInfinity) {
            addEndRangeImage(startElement, range.start.include);
        }
    }

    function addInfinityClass(isStartInfinity, isEndInfinity, range) {
        if ( isStartInfinity &amp;&amp; isEndInfinity ) {
            range.addClass(isStartInfinity ? 'infinityBoth' : '');
        } else {
            range.addClass(isStartInfinity ? 'infinityLeft' : '');
            range.addClass(isEndInfinity ? 'infinityRight' : '');
        }
    }

    function calculateRangeWidth(isEndInfinity, isStartInfinity, difference) {
        var width;

        if (isEndInfinity &amp;&amp; isStartInfinity) {
            width = difference + (presenter.configuration.stepWidth * 2) + 'px';
        } else if (isEndInfinity) {
            width = (difference + presenter.configuration.stepWidth) + 'px';
        } else if (isStartInfinity) {
            width = (difference + presenter.configuration.stepWidth + 2) + 'px';
        } else {
            width = difference + 2 + 'px';
        }

        return width;
    }

    function isValueInfinity(value) {
        return ( value == -Infinity || value == Infinity )
    }

    function setRangeValues(range, shouldAddToDrawn, shouldSendEvents) {
        range.values = [];

        var startValue = Math.min(range.start.value, range.end.value);
        var endValue = Math.max(range.start.value, range.end.value);

        if (startValue == -Infinity) {
            range.values.push(-Infinity);
            if (shouldAddToDrawn) {
                presenter.configuration.drawnRangesData.values.push(-Infinity);
            }
            startValue = presenter.configuration.min;
        }

        if (endValue == Infinity) {
            range.values.push(Infinity);
            if (shouldAddToDrawn) {
                presenter.configuration.drawnRangesData.values.push(Infinity);
            }
            endValue = presenter.configuration.max;
        }

        var stepValue = presenter.configuration.step;

        for ( var i = startValue; i &lt;= endValue; i += stepValue ) {
            range.values.push(i);

            if (shouldAddToDrawn) {
                presenter.configuration.drawnRangesData.values.push(i);
            }
        }
    }

    function convertNdashToMinus(value) {
        return new String(value).replace('&amp;ndash;', '-');
    }

    presenter.convertRangeToString = function( range ) {
        var startInclude = range.start.include ? '&lt;' : '(';
        var endInclude = range.end.include ? '&gt;' : ')';
        var startValue = isValueInfinity(range.start.value) ? '-INF' : convertNdashToMinus(range.start.value);
        var endValue = isValueInfinity(range.end.value) ? 'INF' : convertNdashToMinus(range.end.value);

        return startInclude +
            transformValueToDisplayVersion(startValue, false) +
            '; ' +
            transformValueToDisplayVersion(endValue, false) + endInclude;
    };

    function addEndRangeImage($element, include) {
        var currentImages = $element.find('.rangeImage');
        currentImages.remove();

        var imageContainer = $('&lt;div&gt;&lt;/div&gt;');
        imageContainer.addClass('rangeImage');

        imageContainer.addClass(include ? 'include' : 'exclude');
        $element.append(imageContainer);

        if(!presenter.hideAnswerClicked &amp;&amp; !presenter.isAnswersDisplayed()){
            presenter.parentLeft = imageContainer.parent().css('left');
        }

        return imageContainer;
    }

    presenter.getElementPosition = function (value, axisXWidth, absoluteXRange, axisMin) {
        return ((value - axisMin) / absoluteXRange) * (axisXWidth - (presenter.configuration.stepWidth * 2));
    };

    function getStepText(element) {
        var $text = $('&lt;div&gt;&lt;/div&gt;');
        $text.addClass('stepText');
        $text.html( transformValueToDisplayVersion( element, true ) );
        $text.css('left', - ((element.toString())).length * (4) + 'px');

        return $text;
    }

    function getStepLine() {
        var $stepLine = $('&lt;div&gt;&lt;/div&gt;');
        $stepLine.addClass('stepLine');

        return $stepLine
    }

    function appendTextToStepLine($stepLine, element) {
        var $text = getStepText(element);

        $stepLine.append($text);
    }

    function appendStepLineToAxis($stepLine) {
        presenter.$view.find('.x-axis').append($stepLine);
    }

    function positionStepLineOnAxis($stepLine, element, configuration) {
        var position = presenter.getElementPosition(element, configuration.axisXWidth, configuration.axisAbsoluteRange,
                                                    configuration.axisMin);

        $stepLine.css({
            'left': (position + presenter.configuration.stepWidth) + 'px'
        });
    }

    function isMultiplicationOfCyclicValues (value) {
        function isMultiplication (element) { return (this % element) == 0;}

        var elementIndex = presenter.configuration.axisXFieldValues.indexOf(value);

        return (presenter.configuration.axisXValues.cyclicValues.filter(isMultiplication, elementIndex).length &gt; 0);
    }

    function checkCustomValues(element) {
        var isCustomValue = isMultiplicationOfCyclicValues(element);

        if (!isCustomValue) {
            isCustomValue = (presenter.configuration.axisXValues.fixedValues.indexOf(element) != -1)
        }

        return isCustomValue;
    }

    function shouldAppendTextToStepLine(element, configuration) {
        if (configuration.showAxisXValues) {
            if(configuration.customValuesSet) {
                return checkCustomValues(element)
            }

            return true;
        }

        return false;
    }

    presenter.createStep = function (element) {
        //function for array.forEach, this is binded to object with lineNumber configuration
        /*
            this = {axisXWidth: float, axisMin: float, axisAbsoluteRange: float}
        */

        var $stepLine = getStepLine();
        positionStepLineOnAxis($stepLine, element, this);

        if(shouldAppendTextToStepLine(element, this)) {
            appendTextToStepLine($stepLine, element);
        }

        createClickArea($stepLine, element);

        appendStepLineToAxis($stepLine);
    };

    presenter.createAxisXCustomValues = function () {
        var min = presenter.configuration.min;
        var max = presenter.configuration.max;

        var values = presenter.configuration.axisXValues.fixedValues.concat([]);
        var cyclicValues = presenter.configuration.axisXValues.cyclicValues;

        function createValues (element) {
            var precision = presenter.getNumberPrecision(element);
            var step = element;
            var values = [];

            for(var i = step; i &lt;= max; i += step) {
                values.push(presenter.changeNumberToPrecision(i, precision));
            }

            for(i = step * -1; i &gt;= min; i -= step ) {
                values.push(presenter.changeNumberToPrecision(i, precision));
            }

            return values;
        }

        cyclicValues = cyclicValues.map(createValues);
        for(var i = 0; i &lt; cyclicValues.length; i++) {
            values = values.concat(cyclicValues[i]);
        }

        values = values.filter(function (element){
            if (this.indexOf(element) == -1) {
                this.push(element);
                return true;
            }

            return false;
        }, []);

        if(values.indexOf(0) == -1) values.push(0);

        return values;
    };

    presenter.getAxisConfigurationForCreatingSteps = function () {
        var configuration = {
            axisXWidth: presenter.$view.find('.x-axis').width(),
            axisMin: presenter.configuration.min,
            axisAbsoluteRange: presenter.configuration.max - presenter.configuration.min,
            showAxisXValues: presenter.configuration.showAxisXValues
        };

        if (presenter.configuration.isCustomAxisXValuesSet) {
            configuration.customValuesSet = presenter.configuration.isCustomAxisXValuesSet;
            configuration.customValues = presenter.createAxisXCustomValues();
        }

        return configuration;
    };

    presenter.setStepWidthInConfiguration = function () {
        presenter.configuration.stepWidth = calculateStepWidth(presenter.configuration.axisXFieldValues);
    };

    function setMaxValueInConfiguration () {
        presenter.configuration.max = presenter.maxElement(presenter.configuration.axisXFieldValues);//max()
    }

    presenter.setOnClickAreaListeners = function () {
        if (!presenter.configuration.isPreview &amp;&amp; !presenter.configuration.isDisabled) {
            bindClickAreaListeners(presenter.$view.find('.clickArea'));
        }
    };

    presenter.createSteps = function () {
        setMaxValueInConfiguration();
        presenter.setStepWidthInConfiguration();

        presenter.configuration.axisXFieldValues.forEach(presenter.createStep, presenter.getAxisConfigurationForCreatingSteps());

        presenter.setOnClickAreaListeners();
    };

    function transformValueToDisplayVersion(value, shouldReplaceMinus) {
        var transformed = ('' + value).replace('.', presenter.configuration.separator);
        if (shouldReplaceMinus) {
            transformed = transformed.replace('-', '&amp;ndash;');
        }

        return transformed;
    }

    function checkIsMinLowerThanMax(min, max) {
        var parsedMin = parseRangeStartOrEnd(min, presenter.configuration.separator);
        var parsedMax = parseRangeStartOrEnd(max, presenter.configuration.separator);
        return parsedMin &lt; parsedMax;
    }

    presenter.getState = function () {
        presenter.handleDisplayedAnswers();

        var drawnRangeCopy = $.extend(true, {}, presenter.configuration.drawnRangesData);

        $.each(drawnRangeCopy.ranges, function() {
            this.start.element = null;
            this.end.element = null;
        });

        return JSON.stringify({
            drawnRangesData: drawnRangeCopy,
            isVisible: presenter.configuration.isCurrentlyVisible,
            isDisabled: presenter.configuration.isDisabled,
            points: presenter.checkedPoints
        }, function (key, value) {
            if (value === Infinity) return "Infinity";
            else if (value === -Infinity) return "-Infinity";
            else if (value !== value) return "NaN";
            else return value;
        });
    };

    presenter.redrawRanges = function (rangesToDraw) {
        $.each(rangesToDraw, function () {
            this.start.element = presenter.$view.find('.clickArea[value="' + this.start.value + '"]').parent();
            this.end.element = presenter.$view.find('.clickArea[value="' + this.end.value + '"]').parent();
        });

        $.each(presenter.configuration.shouldDrawRanges, function() {
            presenter.removeRange(this, true);
        });

        presenter.drawRanges(rangesToDraw, true);
    };

    presenter.setState = function (state) {
        if (ModelValidationUtils.isStringEmpty(state)) return;

        var parsedState = JSON.parse(state,
            function (key, value) {
                if (value === "Infinity") return Infinity;
                else if (value === "-Infinity") return -Infinity;
                else if (value === "NaN") return NaN;
                else return value;
            });

        if (presenter.configuration.dontShowRanges) {
            presenter.checkedPoints = parsedState.points;
            drawPoints(presenter.checkedPoints);
        } else {
            presenter.redrawRanges(parsedState.drawnRangesData.ranges);
        }
        presenter.configuration.isCurrentlyVisible = parsedState.isVisible;
        presenter.setVisibility(parsedState.isVisible);

        presenter.configuration.isDisabled = parsedState.isDisabled;

    };

    presenter.reset = function() {
        presenter.handleDisplayedAnswers();

        if (presenter.configuration.dontShowRanges) {
            presenter.$view.find('.rangeImage').remove();
            presenter.checkedPoints = [];
            addInitPoints();
            drawPoints(presenter.checkedPoints);
        } else {
            var rangesToRemove = [].concat(presenter.configuration.drawnRangesData.ranges);
            $.each(rangesToRemove, function() {
                presenter.removeRange(this, true);
            });

            // removing all single dots
            presenter.$view.find('.rangeImage').remove();

            var initRanges = presenter.validateRanges(presenter.configuration.ranges, presenter.configuration.separator);
            presenter.drawRanges(initRanges.shouldDrawRanges, true);

            presenter.configuration.mouseData.clicks = [];
        }

        presenter.configuration.isCurrentlyVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);

        presenter.configuration.isShowErrorsMode = false;
        presenter.configuration.isDisabled = presenter.configuration.isDisabledByDefault;

        presenter.leftShowAnswers = false;
        presenter.parentLeft = false;
    };

    function getCorrectPoints() {
        var correctPoints = [];
        $.each(presenter.configuration.shouldDrawRanges, function() {
            if ($(this)[0].start.value === $(this)[0].end.value) {
                correctPoints.push($(this)[0].start.value)
            }
        });

        $.each(presenter.configuration.otherRanges, function() {
            if ($(this)[0].start.value === $(this)[0].end.value) {
                correctPoints.push($(this)[0].start.value)
            }
        });

        return correctPoints;
    }

    presenter.setShowErrorsMode = function() {
        presenter.handleDisplayedAnswers();

        if(!presenter.configuration.isActivity) {
            return;
        }

        if (presenter.configuration.dontShowRanges) {
            presenter.configuration.isShowErrorsMode = true;
            var correctPoints = getCorrectPoints(),
            $elem;

            if (presenter.checkedPoints.length === 0) {
                addInitPoints();
            }

            presenter.results = {
                correct: 0,
                wrong: 0
            };

            for (var i = 0; i &lt; presenter.checkedPoints.length; i++) {
                var point = presenter.checkedPoints[i];
                if (correctPoints.indexOf(point) !== -1) {
                    $elem = presenter.$view.find(".clickArea[value='" + point + "']");
                    $elem.next().toggleClass('include correctRangeInclude');
                    $elem.next().toggleClass('exclude correctRangeExclude');
                    presenter.results['correct'] += 1;
                } else {
                    $elem = presenter.$view.find(".clickArea[value='" + point + "']");
                    $elem.next().toggleClass('include wrongRangeInclude');
                    $elem.next().toggleClass('exclude wrongRangeExclude');
                    presenter.results['wrong'] += 1;
                }
            }
            presenter.$view.find('.outer').addClass("disable");
        } else if (presenter.configuration.isActivity &amp;&amp; !presenter.configuration.isDisabled) {
            // change single dot to point on axis
            if (presenter.singleDot.value != -1) {
                var newRange = {
                    start: createRangeElement(presenter.singleDot.element, presenter.singleDot.value, true),
                    end: createRangeElement(presenter.singleDot.element, presenter.singleDot.value, true)
                };
                setRangeValues(newRange, true);
                addToDrawnRanges(newRange);
                presenter.singleDot.value = -1;
            }

            presenter.configuration.isShowErrorsMode = true;
            var validated = validateDrawnRanges();

            $.each(validated.correct, function() {
                getSelectedRange(this).addClass('correct');
                addCorrectnessClassToRangeEnds(this, 'correct');
            });

            $.each(validated.wrong, function() {
                getSelectedRange(this).addClass('wrong');
                addCorrectnessClassToRangeEnds(this, 'wrong');
            });

            if (presenter.configuration.mouseData.clicks.length &gt; 0) {
                presenter.configuration.mouseData.clicks[0].element.parent().find('.rangeImage').remove();
            }

            resetClicks();

            presenter.$view.find('.outer').addClass("disable");
        }
    };

    function addCorrectnessClassToRangeEnds(range, includePrefix) {
        var startImage = range.start.element.find('.rangeImage ');
        var endImage = range.end.element.find('.rangeImage');

        if( startImage.hasClass('include') ) {
            startImage.removeClass('include');
            startImage.addClass(includePrefix + 'RangeInclude');
        } else {
            startImage.removeClass('exclude');
            startImage.addClass(includePrefix + 'RangeExclude');
        }

        if( endImage.hasClass('include') ) {
            endImage.removeClass('include');
            endImage.addClass(includePrefix + 'RangeInclude');
        } else {
            endImage.removeClass('exclude');
            endImage.addClass(includePrefix + 'RangeExclude');
        }
    }

    presenter.setWorkMode = function() {
        if ( presenter.configuration.isActivity &amp;&amp; !presenter.configuration.isDisabled ) {
            presenter.configuration.isShowErrorsMode = false;

            presenter.$view.find('.correct').removeClass('correct');
            presenter.$view.find('.wrong').removeClass('wrong');
            presenter.$view.find('.correctRangeExclude, .wrongRangeExclude').removeClass('correctRangeExclude wrongRangeExclude').addClass('exclude');
            presenter.$view.find('.correctRangeInclude, .wrongRangeInclude').removeClass('correctRangeInclude wrongRangeInclude').addClass('include');
            presenter.$view.find('.outer').removeClass("disable");
        }
    };

    function updateScore() {
        var correctPoints = getCorrectPoints();
        presenter.results = {
            correct: 0,
            wrong: 0
        };

        for (var i = 0; i &lt; presenter.checkedPoints.length; i++) {
            var point = presenter.checkedPoints[i];
            if (correctPoints.indexOf(point) !== -1) {
                presenter.results['correct'] += 1;
            } else {
                presenter.results['wrong'] += 1;
            }
        }
    }

    presenter.getScore = function() {
        if (presenter.isProcessing || presenter.isAnswersDisplayed()) {
            return 0;
        }

        resetClicks();

        if (!presenter.configuration.isActivity) {
            return 0;
        }

        if (presenter.configuration.dontShowRanges) {
            updateScore();

            return presenter.results.correct;
        } else {
            var validated = validateDrawnRanges();
            return validated.correct.length;
        }

    };

    presenter.getMaxScore = function () {
        if (presenter.isProcessing || presenter.isAnswersDisplayed()) {
            return 0;
        }

        presenter.handleDisplayedAnswers();

        if (!presenter.configuration.isActivity) {
            return 0;
        }

        return presenter.configuration.otherRanges.length + presenter.configuration.shouldDrawRanges.length;
    };

    presenter.getErrorCount = function () {
        if (presenter.isAnswersDisplayed()) {
            return 0;
        }

        if (!presenter.configuration.isActivity) {
            return 0;
        }

        if (presenter.configuration.dontShowRanges) {
            updateScore();
            return presenter.results.wrong;
        } else {
            var validated = validateDrawnRanges();
            return validated.wrong.length;
        }
    };

    function validateDrawnRanges() {

        var correctSelectedRanges = [];
        var wrongSelectedRanges = [];
        var ranges = presenter.configuration.otherRanges.concat(presenter.configuration.shouldDrawRanges);

        $.each( presenter.configuration.drawnRangesData.ranges, function() {
            var drawnRange = this;

            $.each( ranges, function() {
                setRangeValues(this, false);

                if (compareRanges(this, drawnRange)) {
                    correctSelectedRanges.push(drawnRange);
                }
            });

            if (correctSelectedRanges.indexOf(drawnRange) == -1) {
                wrongSelectedRanges.push(drawnRange)
            }
        });

        return  {
            correct: correctSelectedRanges,
            wrong: wrongSelectedRanges
        }
    }

    function checkIsRangeCorrect( range ) {
        var ranges = presenter.configuration.otherRanges.concat(presenter.configuration.shouldDrawRanges);
        var isCorrect = false;

        setRangeValues(range, false);

        $.each( ranges, function() {
            setRangeValues(this, false);
            if ( compareRanges(this, range) ) {
                isCorrect = true;
                return false; // breaks each loop
            }
        });

        return isCorrect;
    }

    function addToDrawnRanges ( range, shouldSendRanges ) {
        presenter.configuration.drawnRangesData.ranges.push( range );

        if ( !presenter.configuration.isPreview &amp;&amp; !presenter.configuration.isInitialDraw &amp;&amp; !presenter.isAnswersDisplayed()) {
            var rangeString = presenter.convertRangeToString(range);
            var isRangeCorrect = checkIsRangeCorrect(range);
            var eventData = presenter.createEventData(rangeString, false, isRangeCorrect);
            if (shouldSendRanges) {
                eventBus.sendEvent('ValueChanged', eventData);
            }

            if ( shouldSendRanges &amp;&amp; presenter.allRangesCorrect() ) {
                eventData = presenter.createAllOKEventData();
                eventBus.sendEvent('ValueChanged', eventData);
            }
        }
    }

    presenter.allRangesCorrect = function() {
        return presenter.getScore() - presenter.getMaxScore() == 0 &amp;&amp; presenter.getErrorCount() == 0;
    };

    function compareRanges(rangeA, rangeB) {
        return (compareArray(rangeA.values, rangeB.values) &amp;&amp; rangeA.start.include == rangeB.start.include &amp;&amp; rangeA.end.include == rangeB.end.include)
    }

    function compareArray(arrA, arrB) {
        var i = arrA.length;
        if (i != arrB.length) return false;

        while ( i-- ) {
            if (arrA[i] !== arrB[i]) return false;
        }

        return true;
    }

    function parseElement( element ) {
        switch (element) {
            case element.find('.infinity-left').length &gt; 0:
                return presenter.$view.find('.clickArea[value="' + presenter.configuration.min + '"]').parent();
            case element.find('.infinity-right').length &gt; 0:
                return presenter.$view.find('.clickArea[value="' + presenter.configuration.max + '"]').parent();
            default:
                return element;
        }
    }

    function parseRangeStartOrEnd (value, separator) {

        if ( value == '-INF' || value == -Infinity ) {
            return -Infinity;
        }
        else if ( value == 'INF' || value == Infinity ) {
            return Infinity;
        }
        else {
            var parsedValue = presenter.parseValueWithSeparator(value, separator);
            return parseFloat( parsedValue );
        }
    }

    presenter.validateDecimalSeparator = function (decimalSeparator) {
        if ( ModelValidationUtils.isStringEmpty(decimalSeparator) ) {
            return {value: '.', isValid: true};
        }

        if ( $.trim(decimalSeparator) == ';' ) {
            return {value: null, isValid: false, errorCode: 'DSE01'};
        }

        return {value: decimalSeparator, isValid: true};
    };

    presenter.validateRanges = function (ranges, separator) {
        if (separator == undefined) {
            separator = '.';
        }

        var rangesList = Helpers.splitLines(ranges);
        separator = escapeRegexSpecialCharacters(separator);
        var rangesPattern = new RegExp('(\\(|&lt;){1}[(?P \\d|(-){1}INF' + separator + ')-]+;[(?P \\d|(-){1}INF' + separator + ')-]+(\\)|&gt;){1};[ ]*(0|1){1}', 'i'); // matches i.e. (1; 0); 0 or &lt;2; 15); 1, &lt;-INF; 10); 1, &lt;1.5; 2.5); 0
//        var rangesPattern = /(\(|&lt;){1}[(?P \d|(-){1}INF\.)-]+;[(?P \d|(-){1}INF\.)-]+(\)|&gt;){1};[ ]*(0|1){1}/i;
        var validatedShouldDrawRanges = [];
        var validatedOtherRanges = [];
        var isError = false,
            errorCode = '';

        $.each(rangesList, function() {
            var rangeString = this.toString();

            if( !rangesPattern.test(rangeString) ) {
                isError = true;
                errorCode = 'RAN01';

                return false; // Breaks jQuery.each loop
            }

            var regexResult = rangesPattern.exec(rangeString)[0];
            var brackets = regexResult.match(/[\(\)&lt;&gt;]+/g);
            var onlyNumbersAndCommas = regexResult.replace(/[ \(\)&lt;&gt;]*/g, '');
            var onlyNumbers = onlyNumbersAndCommas.split(';');
            var min = parseRangeStartOrEnd(onlyNumbers[0], separator);
            var max = parseRangeStartOrEnd(onlyNumbers[1], separator);
            var minInclude = brackets[0] == '&lt;' || min == -Infinity;
            var maxInclude = brackets[1] == '&gt;' || max == Infinity;
            var shouldDrawRange = onlyNumbers[2] == '1';

            if ( (min &gt; max)
                || (min == Infinity &amp;&amp; max == Infinity)
                || (min == -Infinity &amp;&amp; max == -Infinity) ) {

                isError = true;
                errorCode = 'MIN/MAX01';

                return false; // Breaks jQuery.each loop
            }

            var validatedRange = {
                start: { value : min, include: minInclude, element: null },
                end: { value: max, include: maxInclude, element: null }
            };

            if (shouldDrawRange) {
                validatedShouldDrawRanges.push(validatedRange);
            } else {
                validatedOtherRanges.push(validatedRange);
            }

            return true; // jQuery.each continue statement
        });

        return {
            isValid: true,
            isError: isError,
            errorCode: errorCode,
            shouldDrawRanges : validatedShouldDrawRanges,
            otherRanges : validatedOtherRanges
        };
    };

    presenter.validateRangesWithAxisXField = function (ranges, axisXFieldValues) {
        var allRanges = ranges.otherRanges.concat(ranges.shouldDrawRanges);

        function checkRange(range) {
            if ( (axisXFieldValues.indexOf(range.start.value) == -1 || axisXFieldValues.indexOf(range.end.value) == -1)
                &amp;&amp; (!isValueInfinity(range.start.value) &amp;&amp; !isValueInfinity(range.end.value) )) {

                presenter.configuration.isError = true;
                DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.errorCodes, 'RAN02');

                return false;
            }

            return true;
        }

        if (!allRanges.every(checkRange)) {
            return presenter.getErrorObject("RAN02");
        }

        var lastElement = presenter.maxFromArray(axisXFieldValues);
        if ( axisXFieldValues.indexOf(lastElement) == -1 ) {
            presenter.configuration.isError = true;
            DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.errorCodes, 'MAX04');
        }

        return {isValid: true, value: allRanges};
    };

    presenter.maxFromArray = function (array_of_numbers) {
        if (array_of_numbers.length == 0) {throw "ValueError: maxFromArray() arg is an empty array";}

        var max = array_of_numbers[0];

        for(var i = 0; i &lt; array_of_numbers.length; i++){
            if (array_of_numbers[i] &gt; max) {max = array_of_numbers[i];}
        }

        return max;
    };

    presenter.minFromArray = function (array_of_numbers) {
        if (array_of_numbers.length == 0) {throw "ValueError: minFromArray() arg is an empty array";}

        var min = array_of_numbers[0];

        for(var i = 0; i &lt; array_of_numbers.length; i++){
            if (array_of_numbers[i] &lt; min) {min = array_of_numbers[i];}
        }

        return min;
    };

    function escapeRegexSpecialCharacters(value) {
        return (value + '').replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&amp;'); // escape regex special characters
    }

    presenter.validateValueWithSeparator = function (value, separator) {
        var escapedSeparator = escapeRegexSpecialCharacters(separator);

        var pattern = new RegExp('^(-{0,1}\\d+)' + escapedSeparator + '{0,1}\\d*$', 'i');

        if ( pattern.test(value) ) {
            return {
                'isValid' : true,
                'value' : presenter.parseValueWithSeparator(value, separator),
                'precision' : getPrecision(value, separator)
            }
        } else {
            return {
                'isValid' : false,
                'value' : null
            }
        }
    };

    function getPrecision(value, separator) {
        var splitted = value.split(separator);
        if ( splitted.length == 1 ) {
            return 0;
        } else {
            return splitted[1].length;
        }
    }

    presenter.parseValueWithSeparator = function (value, separator) {
        return (value + '').replace(separator, '.');
    };

    presenter.getErrorObject = function (errorCode) {
        return {isValid: false, errorCode: errorCode, isError: true};
    };

    presenter.validateMin = function (model, separator) {
        if(ModelValidationUtils.isStringEmpty(model['Min'])) {
            return presenter.getErrorObject("MIN01");
        }

        var validatedMinWithSeparator = presenter.validateValueWithSeparator(model['Min'], separator);
        if(!validatedMinWithSeparator.isValid) {
            return presenter.getErrorObject("MIN03");
        }

        validatedMinWithSeparator = ModelValidationUtils.validateFloat( validatedMinWithSeparator.value );
        if ( !validatedMinWithSeparator.isValid) {
            return presenter.getErrorObject("MIN02");
        }


        return {isValid: true, value: validatedMinWithSeparator.parsedValue};
    };

    presenter.validateMax = function (model, separator) {
        if( ModelValidationUtils.isStringEmpty(model['Max']) ) {
            return presenter.getErrorObject("MAX01");
        }

        var validatedMaxWithSeparator = presenter.validateValueWithSeparator(model['Max'], separator);

        if(!validatedMaxWithSeparator.isValid) {
            return presenter.getErrorObject("MAX03");
        }

        validatedMaxWithSeparator = ModelValidationUtils.validateFloat( validatedMaxWithSeparator.value );
        if ( !validatedMaxWithSeparator.isValid ) {
            return presenter.getErrorObject("MAX02");
        }

        return {isValid: true, value: validatedMaxWithSeparator.parsedValue};
    };

    presenter.validateModel = function(model) {
        var separator = presenter.validateDecimalSeparator(model['Decimal Separator']);
        if (!separator.isValid) {
            return presenter.getErrorObject("DSE01");
        }

        var validatedMin = presenter.validateMin(model, separator.value);
        if ( !validatedMin.isValid ) {
            return validatedMin;
        }

        var validatedMax = presenter.validateMax(model, separator.value);
        if ( !validatedMax.isValid ) {
            return validatedMax;
        }

        if( !checkIsMinLowerThanMax(validatedMin.value, validatedMax.value) ) {
            return presenter.getErrorObject("MIN/MAX01");
        }

        var validatedStep = presenter.validateStep(model, separator, validatedMax.value, validatedMin.value);
        if (!validatedStep.isValid) {
            return validatedStep;
        }

        var axisXFieldValues = presenter.createAxisXFieldValues(validatedMin.value, validatedMax.value, validatedStep.value);

        var validatedRanges = presenter.validateRanges(model["Ranges"], separator.value);
        if(!validatedRanges.isValid) {
            return validatedRanges;
        }

        var validatedRangesWithAxisXField = presenter.validateRangesWithAxisXField(validatedRanges, axisXFieldValues);
        if (!validatedRangesWithAxisXField.isValid) {
            return validatedRangesWithAxisXField;
        }

        var validatedIsActivity = !ModelValidationUtils.validateBoolean(model['Not Activity']);

        var addonConfiguration = {
            isDecimalSeparatorSet: !ModelValidationUtils.isStringEmpty(separator.value),
            decimalSeparator: separator.value,
            max: validatedMax.value,
            min: validatedMin.value
        };

        var validatedAxisXValues = presenter.validateAxisXValues(model, addonConfiguration);

        if(!validatedAxisXValues.isValid) {
            return validatedAxisXValues;
        }

        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        var isDisabled = ModelValidationUtils.validateBoolean(model['Disable']);

        if (isDisabled) {
            presenter.$view.find('.outer').addClass('disable');
        }

        return {
            isValid: true,
            isError : false,
            min : validatedMin.value,
            max : validatedMax.value,
            shouldDrawRanges : validatedRanges.shouldDrawRanges,
            otherRanges : validatedRanges.otherRanges,
            isActivity : validatedIsActivity,
            step : validatedStep.value,
            showAxisXValues : ModelValidationUtils.validateBoolean(model['Show Axis X Values']),
            axisXValues : validatedAxisXValues.value,
            mouseData : {
                clickedRanges : [],
                clicks : [],
                twoClickedRangesCount : 0
            },
            drawnRangesData : {
                isDrawn : false,
                ranges : [],
                values : []
            },
            touchData : {
                lastEvent : null
            },
            isShowErrorsMode : false,
            isCurrentlyVisible: isVisible,
            isVisibleByDefault: isVisible,
            notCurrentSelectedRange : null,
            addonID : model['ID'],
            isInitialDraw : true,
            isDisabled: isDisabled,
            isDisabledByDefault: isDisabled,
            separator: separator.value,
            axisXFieldValues: axisXFieldValues,
            allRanges: validatedRangesWithAxisXField.value,
            isCustomAxisXValuesSet: validatedAxisXValues.isCustomAxisXValuesSet,
            ranges: model["Ranges"],
            dontShowRanges: ModelValidationUtils.validateBoolean(model["Don't show ranges"]) || false
        };
    };

    presenter.isZeroInRange = function(min, max) {
        return (min &lt;= 0 &amp;&amp; max &gt;= 0) || (min &lt; 0 &amp;&amp; max == 0) || (min == 0 &amp;&amp; max &gt; 0);
    };

    function getAxisXValuesErrors(fixedValues, cyclicValues, addonConfiguration) {
        if((fixedValues.filter(isNaN).length + cyclicValues.filter(isNaN).length) &gt; 0) {
            return presenter.getErrorObject("AXV_04");
        }

        if(!cyclicValues.every(function (value) {return (value &gt;= 0)})) {
            return presenter.getErrorObject("AXV_01");
        }

        if(!fixedValues.every(function (value) {return (value &gt;= addonConfiguration.min);})) {
            return presenter.getErrorObject("AXV_02");
        }

        if(!fixedValues.every(function (value) {return (value &lt;= addonConfiguration.max);})) {
            return presenter.getErrorObject("AXV_03");
        }

        return {isValid: true};
    }

    function parseAxisXValuesFromModel(model, addonConfiguration) {
        return model["Axis X Values"].split(";").map(function (element) {
            element.trim();
            if (addonConfiguration.isDecimalSeparatorSet) {
                return element.replace(addonConfiguration.decimalSeparator, ".");
            }

            return element;
        });
    }

    function filterCyclicValues (value) {
        return (value.charAt(value.length - 1) == "*");
    }

    function filterFixedValuesBasedOnCyclic (element) {
        return (this.indexOf(element) == -1);
    }

    presenter.validateAxisXValues = function (model, addonConfiguration) {

        if(ModelValidationUtils.isStringEmpty(model["Axis X Values"])) {
            return {isValid: true, isCustomAxisXValuesSet: false, value: {}};
        }

        var values = parseAxisXValuesFromModel(model, addonConfiguration);

        var cyclicValues = values.filter(filterCyclicValues);

        var fixedValues = values.filter(filterFixedValuesBasedOnCyclic, cyclicValues).map(Number);

        cyclicValues = cyclicValues.map(function (value) {
            return Number(value.slice(0, value.length -1));
        });

        var axisXValuesErrors = getAxisXValuesErrors(fixedValues, cyclicValues, addonConfiguration);
        if (!axisXValuesErrors.isValid) {
            return axisXValuesErrors;
        }

        cyclicValues = cyclicValues.map(function (value) {
            if(value == 0) {return 1};
            return value;
        });

        function isDuplicate(value) {
            return (this.filter(function (currentValue) {return (value == currentValue);}).length == 1);
        }

        if((!cyclicValues.every(isDuplicate, cyclicValues)) || (!fixedValues.every(isDuplicate, fixedValues))) {
            return presenter.getErrorObject("AXV_05");
        }

        return {isValid: true, isCustomAxisXValuesSet: true, value: {cyclicValues: cyclicValues, fixedValues: fixedValues}};
    };

    presenter.createAxisXFieldValues = function (min, max, step) {
        var precision = presenter.maxElement([presenter.getNumberPrecision(step), presenter.getNumberPrecision(min), presenter.getNumberPrecision(max)]);   //max()
        var values = [];
        var i;

        function changePrecision(value) {return presenter.changeNumberToPrecision(value, precision);}

        for (i = min; i &lt;= max; i += step) {
            values.push(i);
        }

        if (presenter.isZeroInRange(min, max)) {
            if (values.indexOf(0) == -1) {
                values.push(0);
            }

        }

        return values.map(changePrecision).sort(function(a, b) {
           return a - b;
        });
    };

    presenter.getNumberPrecision = function(value) {
        value = value.toString();
        value = value.split(".");

        var len;
        try {
            len = value[1].length;
        } catch (_){
            len = 0;
        }

        return len;
    };

    presenter.changeNumberToPrecision = function (value, precision) {
        //toFixed value rounds up to closest number eg. 23.6xx.toFixed(0) -&gt; 24, when we want get 23
        if (precision == 0) {
            return parseInt(value, 10);
        }
        return Number(value.toFixed(precision));
    };

    presenter.findStartingPointInField = function (min, max, step) {
        var precision = presenter.getNumberPrecision(step);
        var startingPoint;
        if (min &gt; 0) {
            if (min % step == 0) {
                return {startingPoint: min, fieldEnd: max};
            }

            startingPoint = ((parseInt(min / step) * step) + step);
            return {startingPoint: presenter.changeNumberToPrecision(startingPoint, precision), fieldEnd: max};
        }

        if(min &lt; 0) {
            if (max % step == -0) {
                return {startingPoint: max, fieldEnd: min};
            }

            startingPoint = ((parseInt(max / step) *  step) - step);
            return {startingPoint: presenter.changeNumberToPrecision(startingPoint, precision), fieldEnd: min};
        }
    };

    presenter.abs = function (value) {
        if (value &lt; 0) {
            return value * -1;
        }

        return value;
    };

    presenter.validateStep = function (model, separator, max, min) {
        if(ModelValidationUtils.isStringEmpty(model['Step'])) {
            return {isValid: true, value : 1, precision : 0};
        }

        var validatedStep = presenter.validateValueWithSeparator( model['Step'], separator.value );

        var precision = validatedStep.precision;

        if (!validatedStep.isValid) {
            return presenter.getErrorObject('STEP02');
        }

        validatedStep = ModelValidationUtils.validateFloatInRange(validatedStep.value,
            presenter.maxFromArray([max, min].map(presenter.abs)), 0, precision);
        validatedStep.precision = precision;

        if(validatedStep.value == 0) {
            return presenter.getErrorObject("STEP03");
        }

        if (!validatedStep.isValid) {
            return presenter.getErrorObject("STEP01");
        }

        return {isValid: true, value: validatedStep.parsedValue, precision: validatedStep.precision};
    };

    presenter.isMultiplication = function (value) {
        var pattern = new RegExp('^[\\d]+\\*{1}$', 'i');
        return pattern.test(value);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'drawRange' : presenter.drawRange,
            'enable': presenter.enable,
            'disable': presenter.disable,
            'showAnswers': presenter.showAnswers,
            'hideAnswers': presenter.hideAnswers
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.children('div').css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.configuration.isCurrentlyVisible = true;
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.configuration.isCurrentlyVisible = false;
        presenter.setVisibility(false);
    };

    presenter.setDisableState = function(isDisabled) {
        var element = presenter.$view.find('.outer');

        if (isDisabled) {
            element.addClass("disable");
        } else {
            element.removeClass("disable");
        }

        presenter.configuration.isDisabled = isDisabled;
    };

    presenter.enable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.setDisableState(false);

        bindClickAreaListeners( presenter.$view.find('.clickArea') );
        presenter.bindInfinityAreas();
        resetClicks();
    };

    presenter.disable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.setDisableState(true);

        removeAllClickListeners();
    };

    presenter.drawRange = function (rangeList) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        rangeList = [].concat(rangeList);
        var rangeString = rangeList.join('\n\r');
        var validatedRanges = presenter.validateRanges(rangeString);

        $.each(validatedRanges.shouldDrawRanges, function() {
            removeRangesBetweenRange(this);
        });

        presenter.drawRanges(validatedRanges.shouldDrawRanges, false);

    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
        eventBus = controller.getEventBus();
    };

    presenter.createEventData = function (rangeString, isRemove, isRangeCorrect) {
        return {
            'source': presenter.configuration.addonID,
            'item': rangeString,
            'value': isRemove ? '0' : '1',
            'score': isRangeCorrect ? '1' : '0'
        };
    };

    presenter.createActionEventData = function(item, action, isCorrect) {
        return {
            'source': presenter.configuration.addonID,
            'item': item,
            'value': action,
            'score': isCorrect ? '1' : '0'
        };
    }

    presenter.createAllOKEventData = function () {
        return {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };
    };

    presenter.getActivitiesCount = function () {
        if (presenter.configuration.dontShowRanges) {
            return getCorrectPoints().length;
        }

        return presenter.configuration.otherRanges.length;
    }

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case 'ShowAnswers':
                presenter.showAnswers();
                break;

            case 'HideAnswers':
                presenter.hideAnswers();
                break;

            case 'GradualShowAnswers':
                presenter.gradualShowAnswers(eventData);
                addListenerWithTimeout();
                break;

            case 'GradualHideAnswers':
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.showAnswers = function () {
        if (!presenter.configuration.isActivity){
            return;
        }

        presenter.isShowAnswersActive = true;
        presenter.setWorkMode();

        if (presenter.configuration.dontShowRanges) {
            presenter.$view.find('.rangeImage').remove();
            var correctPoints = getCorrectPoints();

            drawPoints(correctPoints);
            presenter.$view.find('.rangeImage').each(function() {
                $(this).addClass('show-answers');
            });
        } else {
            presenter.saveUserRanges();
            presenter.removeRanges();

            presenter.drawRanges(presenter.configuration.shouldDrawRanges, true);
            presenter.drawRanges(presenter.configuration.otherRanges, true);

            $.each(presenter.configuration.drawnRangesData.ranges, function() {
                getSelectedRange(this).addClass('show-answers');
            });
        }
        presenter.$view.find('.outer').addClass("disable");
    };

    function drawPoints(pointsToDraw, shouldRemovePreviousRanges = true) {
        if (shouldRemovePreviousRanges) {
            presenter.$view.find('.rangeImage').remove();
        }

        presenter.$view.find('.clickArea').each(function() {
            var $imageContainer = $('&lt;div&gt;&lt;/div&gt;');
            $imageContainer.addClass('rangeImage exclude include');
            if (pointsToDraw.indexOf($(this).attr('value')) != -1) {
                $(this).parent('').append($imageContainer);
            }
        });
    }

    presenter.hideAnswers = function () {
        if(!presenter.configuration.isActivity || !presenter.isShowAnswersActive) {
            return;
        }
        presenter.hideAnswerClicked = true;
        presenter.$view.find('.show-answers').removeClass('show-answers');

        if (presenter.configuration.dontShowRanges) {
            drawPoints(presenter.checkedPoints);
        } else {
            presenter.removeRanges();
            presenter.redrawRanges(presenter.currentRanges.ranges);
            presenter.displayUserRanges();
        }

        presenter.hideAnswerClicked = false;
        presenter.isShowAnswersActive = false;
        presenter.$view.find('.outer').removeClass("disable");
        presenter.currentRanges = null;
    };
    
    presenter.gradualShowAnswers = function (eventData) {
        if (eventData.moduleID !== presenter.configuration.addonID) {
            return;
        }

        presenter.isProcessing = true;
        presenter.isGradualShowAnswersActive = true;
        presenter.setWorkMode();

        if (presenter.configuration.dontShowRanges) {
            if (presenter.isFirstRun) {
                presenter.$view.find('.rangeImage').remove();
                presenter.isFirstRun = false;
            }
            const correctPoints = [getCorrectPoints()[eventData.item]];

            drawPoints(correctPoints, false);
            presenter.$view.find('.rangeImage').each(function() {
                $(this).addClass('show-answers');
            });
        } else {
            if (!presenter.currentRanges) {
                presenter.saveUserRanges();
                presenter.removeRanges();
            }

            const range = [presenter.configuration.otherRanges[eventData.item]];

            presenter.drawRanges(presenter.configuration.shouldDrawRanges, true);
            presenter.drawRanges(range, true);

            $.each(presenter.configuration.drawnRangesData.ranges, function() {
                getSelectedRange(this).addClass('show-answers');
            });
        }

        presenter.$view.find('.outer').addClass("disable");
        presenter.isProcessing = false;
    }

    presenter.gradualHideAnswers = function () {
        if(!presenter.configuration.isActivity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.$view.find('.show-answers').removeClass('show-answers');
        presenter.isGradualShowAnswersActive = false;

        if (presenter.configuration.dontShowRanges) {
            drawPoints(presenter.checkedPoints);
            presenter.isFirstRun = true;
        } else {
            presenter.removeRanges();
            presenter.redrawRanges(presenter.currentRanges.ranges);
            presenter.displayUserRanges();
        }

        presenter.$view.find('.outer').removeClass("disable");
        presenter.currentRanges = null;
    }

    presenter.saveUserRanges = function () {
        presenter.currentRanges = jQuery.extend(true, {}, presenter.configuration.drawnRangesData);

        presenter.leftShowAnswers = presenter.parentLeft;
        if (presenter.leftShowAnswers) {
            presenter.$view.find('.rangeImage').each(function () {
                if (parseInt($(this).parent()[0].style.left, 10).toFixed(1) == parseInt(presenter.leftShowAnswers, 10).toFixed(1)) {
                    presenter.rangeShowAnswers = $(this);
                }
            });
        }
    }

    presenter.removeRanges = function () {
        presenter.$view.find('.rangeImage').remove();
        const rangesToRemove = [].concat(presenter.configuration.drawnRangesData.ranges);
        $.each(rangesToRemove, function() {
            presenter.removeRange(this, true);
        });
    }

    presenter.displayUserRanges = function () {
        if(presenter.leftShowAnswers){
            presenter.$view.find('.stepLine').each(function () {
                if(parseInt($(this)[0].style.left, 10).toFixed(1) == parseInt(presenter.leftShowAnswers, 10).toFixed(1)){
                    $(this).append(presenter.rangeShowAnswers);
                }
            });
        }
    }

    function addListenerWithTimeout() {
        setTimeout(() =&gt; {
            presenter.addListenerOnHover();
        }, 200);
    }
    
    presenter.addListenerOnHover = function () {
        const area = presenter.$view.find('.clickArea');
        area.on('mouseleave', function (e) {
            e.stopPropagation();
            hideCurrentMousePosition();
        });

        area.on('mouseenter', function (e) {
            e.stopPropagation();
            e.preventDefault();
            displayCurrentMousePosition($(e.target));
        });
    }

    presenter.handleDisplayedAnswers = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
    }

    presenter.isAnswersDisplayed = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Line_Selection" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Lines" name="Lines" nameLabel="Line_Selection_property_lines" type="text"/>
		<property displayName="Is Activity" name="IsActivity" nameLabel="Line_Selection_property_is_activity" type="boolean"/>
        <property displayName="Is Disabled" name="IsDisabled" nameLabel="Line_Selection_property_is_disabled" type="boolean"/>
        <property displayName="Single Mode" name="SingleMode" nameLabel="Line_Selection_property_single_mode" type="boolean"/>
	</model>
<css>.lines_wrapper .line {
    stroke:#05B8CC;
    stroke-width:10;
    stroke-linecap: round;
    cursor: pointer;
}
.lines_wrapper .line.selected {
    stroke:#941B80;
}
.lines_wrapper .line.correct {
    stroke:green;
}
.lines_wrapper .line.wrong {
    stroke:red;
}
.lines_wrapper .line.show_answers_ok {
    stroke:black;
}
.lines_selection.disabled {
    opacity:0.3;
}
.lines_selection .coordinates {
    color: black;
    font-size: 14px;
    border: 1px solid black;
    padding-left: 20px;
    width: 100px;
}
</css><view>&lt;div class="lines_selection"&gt;&lt;/div&gt;
</view><preview>&lt;div class="lines_selection"&gt;&lt;/div&gt;
</preview><presenter>function AddonLine_Selection_create(){
    var presenter = function(){}
    presenter.error = false;
    presenter.isErrorMode = false;
    presenter.isStarted = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;

    presenter.executeCommand = function(name, params) {
        switch (name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'isAllOK'.toLowerCase():
                presenter.isAllOK();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'select'.toLowerCase():
                presenter.select(params[0]);
                break;
            case 'deselect'.toLowerCase():
                presenter.deselect(params[0]);
                break;
        }
    };

    presenter.ERROR_CODES = {
        'lines_error' : "Error in lines' defition.",
        'lines_empty' : 'Property Lines cannot be empty!',
        'points_out' : 'Ending points are outside  the addon!'
    };

    presenter.disable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        if (!(presenter.$view.find('.disabled').length &gt; 0)) {
            presenter.disabled = true;
            presenter.$view.find('.lines_selection').addClass('disabled');
        }
    };

    presenter.enable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.disabled = false;
        presenter.$view.find('.disabled').removeClass('disabled');
    };

    presenter.updateDisability = function(){
        presenter.disabled ? presenter.disable() : presenter.enable();
    };

    presenter.hide = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        presenter.isVisible ? presenter.show() : presenter.hide();
    };

    presenter.isAllOK = function() {
        presenter.handleDisplayedAnswers();
        if (presenter.getScore() === presenter.getMaxScore()
            &amp;&amp; presenter.getErrorCount() === 0) {
            return true;
        }
        return false;
    };

    presenter.isAttempted = function() {
        presenter.handleDisplayedAnswers();
        return presenter.isStarted;
    };

    presenter.select = function(index) {
        index--;
        presenter.isStarted = true;
        var line = presenter.$view.find('#line_'+index);
        if (line.attr('class') != 'line selected') {
            if (presenter.singleMode) {
                presenter.$view.find('.selected').attr('class','line');
                presenter.selected = [];
            }
            line.attr('class','line selected');
            presenter.selected.push(index);
        }
    };

    presenter.deselect = function(index) {
        index--;
        presenter.isStarted = true;
        var line = presenter.$view.find('#line_'+index);
        if (line.attr('class') == 'line selected') {
            if (presenter.singleMode)
                presenter.selected = [];
            else
                presenter.selected.splice(presenter.selected.indexOf(index),1);
            line.attr('class','line');
        }
    };

    presenter.initiate = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        presenter.wrapper = presenter.$view.find('.lines_selection');
        presenter.activity = ModelValidationUtils.validateBoolean(presenter.model['IsActivity']);
        presenter.disabled = ModelValidationUtils.validateBoolean(presenter.model['IsDisabled']);
        presenter.initDisabled = presenter.disabled;
        presenter.isVisible = ModelValidationUtils.validateBoolean(presenter.model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        presenter.singleMode = ModelValidationUtils.validateBoolean(presenter.model['SingleMode']);
        if (presenter.drawLines(presenter.model['Lines'])) {
            presenter.wrapper.text(presenter.ERROR_CODES[presenter.error])
        } else {
            presenter.updateDisability();
        }
    };

    presenter.drawLines = function(string) {
        if (string == '' || string == undefined) {
            presenter.error = 'lines_empty';
            return true;
        };
        var oneLine,points = [];
        var testString = /^\d+;\d+\-\d+;\d+\-[01]$/;
        var Lines = Helpers.splitLines(string);
        for(var i = 0; i &lt; Lines.length; i++) {
            if (!testString.test(Lines[i])) {
                presenter.error = 'lines_error';
                return true;
            };
        }
        var Width = presenter.wrapper.width();
        var Height = presenter.wrapper.parent().height();
        var $svg = '&lt;svg height="'+Height+'" width="'+Width+'" version="1.1" xmlns:xlink="http://www.w3.org/2000/svg" class="lines_wrapper"&gt;';
        for (i = 0; i &lt; Lines.length; i++) {
            oneLine = Lines[i].split('-');
            points[0] = oneLine[0].split(';');
            points[1] = oneLine[1].split(';');
            if (presenter.activity) presenter.answers.push(oneLine[2]);
            if (points[0][0] &lt; 0 || points[0][0] &gt; Width || points[1][0] &lt; 0 || points[1][0] &gt; Width || points[0][1] &lt; 0 || points[0][1] &gt; Height || points[1][1] &lt; 0 || points[1][1] &gt; Height) {
                presenter.error = 'points_out';
                return true;
            }
            $svg += '&lt;line id="line_'+(i)+'" class ="line" x1="' + points[0][0] +'" y1="'+points[0][1]+'" x2="'+points[1][0]+'" y2="'+points[1][1]+'"&gt;&lt;/line&gt;';
        }
        $svg += '&lt;/svg&gt;';
        presenter.wrapper.prepend($svg);
    };

    presenter.run = function(view, model){
        presenter.answers = [];
        presenter.selected = [];
        var presentId, item, value, score;
        presenter.initiate(view, model);
        presenter.updateVisibility();
        presenter.$view.find('.line').on('click', function(e){
            e.stopPropagation();
            e.preventDefault();
            if (!presenter.disabled &amp;&amp; !presenter.isErrorMode &amp;&amp; !presenter.isAnswersDisplayed()) {
                presenter.isStarted = true;
                presentId = $(this).attr('id').substr(5);
                if ($(this).attr('class') == 'line selected') {
                    $(this).attr('class','line');
                    if (presenter.singleMode)
                        presenter.selected = [];
                    else
                        presenter.selected.splice(presenter.selected.indexOf(presentId),1);
                    value = 0;
                } else {
                    if (presenter.singleMode) {
                        presenter.$view.find('.selected').attr('class','line');
                        presenter.selected = [];
                    }
                    $(this).attr('class','line selected');
                    presenter.selected.push(presentId);
                    value = 1;
                }
                if (presenter.activity)
                    score = presenter.answers[presentId]
                else
                    score = 0;
                item = parseInt(presentId) + 1;
                presenter.triggerEvent(item,value,score);
            }
        });
        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        events.forEach((eventName) =&gt; {
            presenter.eventBus.addEventListener(eventName, this);
        });
    };

    presenter.onEventReceived = function (eventName, eventData) {
         switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(item,value,score) {
        return {
            source : presenter.addonID,
            item : item,
            value : value,
            score : score
        };
    };

    presenter.triggerEvent = function(item, value, score) {
        var eventData = presenter.createEventData(item, value, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
        if (presenter.getScore() == presenter.getMaxScore() &amp;&amp; presenter.activity &amp;&amp; presenter.getErrorCount() == 0) {
            eventData = presenter.createEventData('all','','');
            presenter.eventBus.sendEvent('ValueChanged', eventData);
        }
    };

    presenter.createPreview = function(view, model){
        presenter.answers = [];
        presenter.selected = [];
        presenter.initiate(view, model);
        var coordinations = {x:0, y:0};
        if (!presenter.error) {
            var coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
                xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
                yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;');
            coordinatesContainer.addClass('coordinates');
            coordinatesContainer.append(xContainer).append(yContainer);
            presenter.wrapper.append(coordinatesContainer);
            function setCalculatedPosition(e) {
                coordinations.x = e.originalEvent.pageX || e.originalEvent.touches[0].pageX;
                coordinations.y = e.originalEvent.pageY || e.originalEvent.touches[0].pageY;
                presenter.mouseSX = parseInt(coordinations.x,10) - parseInt(presenter.wrapper.offset().left,10);
                presenter.mouseSY = parseInt(coordinations.y,10) - parseInt(presenter.wrapper.offset().top,10);
                xContainer.find('.value').html(presenter.mouseSX);
                yContainer.find('.value').html(presenter.mouseSY);
            }

            var doesElementExist = function() {
                var $moduleSelector = $('.moduleSelector[data-id="'+presenter.addonID+'"]');

                if ($moduleSelector.length &gt; 0) {
                    $moduleSelector.on('mousemove', function(e) {
                        setCalculatedPosition(e);
                    });

                    clearInterval(interval);
                }
            };

            var interval = setInterval(function() { doesElementExist(); }, 500);

            presenter.wrapper.on('mousemove', function(e) {
                setCalculatedPosition(e)
            });
        }
    };

    presenter.setShowErrorsMode = function(){
        presenter.handleDisplayedAnswers();
        presenter.isErrorMode = true;
        if (presenter.activity) {
            for (var i = 0; i &lt;	presenter.answers.length; i++) {
                if (presenter.answers[presenter.selected[i]] == 1)
                    presenter.$view.find('#line_'+presenter.selected[i]).addClass('correct');
                else
                    presenter.$view.find('#line_'+presenter.selected[i]).addClass('wrong');
            }
        } else
            return 0;
    };

    presenter.setWorkMode = function(){
        presenter.isErrorMode = false;
        presenter.$view.find('.correct').removeClass('correct');
        presenter.$view.find('.wrong').removeClass('wrong');
    };

    presenter.showAnswers = function () {
        if (!presenter.activity) {
            return;
        }

        presenter.setWorkMode();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isShowAnswersActive = true;

        prepareViewToShowAnswers();
        presenter.answers.forEach((answer, index) =&gt; {
            answer == 1 &amp;&amp; presenter.$view.find("#line_" + index).addClass("show_answers_ok");
        });
    };

    presenter.gradualShowAnswers = function (eventData) {
        if (!presenter.activity ||
            eventData.moduleID !== presenter.addonID) {
            return;
        }

        const answerToShowIndex = parseInt(eventData.item, 10);

        presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive = true;

        prepareViewToShowAnswers();
        let currentAnswerIndex = 0;
        for (let i = 0; i &lt; presenter.answers.length; i++) {
            if (presenter.answers[i] == 0) {
                continue;
            }

            if (currentAnswerIndex === answerToShowIndex) {
                presenter.$view.find("#line_" + i).addClass("show_answers_ok");
                return;
            }
            currentAnswerIndex++;
        }
    };

    function prepareViewToShowAnswers() {
        presenter.$view.find(".lines_selection").addClass("show_answers");
        presenter.$view.find(".selected").removeClass("selected");
    }

    presenter.hideAnswers = function () {
        if (!presenter.activity || !presenter.isShowAnswersActive) {
            return;
        }
        presenter.isShowAnswersActive = false;
        _hideAnswers();
    };

    presenter.gradualHideAnswers = function () {
        if (!presenter.activity || !presenter.isGradualShowAnswersActive) {
            return;
        }
        presenter.isGradualShowAnswersActive = false;
        _hideAnswers();
    };

    function _hideAnswers() {
        presenter.$view.find(".lines_selection").removeClass("show_answers");
        presenter.$view.find(".show_answers_ok").removeClass("show_answers_ok");
        for (let i = 0; i &lt; presenter.selected.length; i++) {
            presenter.$view.find("#line_" + presenter.selected[i]).addClass('selected');
        }
    }

    presenter.reset = function(){
        presenter.setWorkMode();
        presenter.handleDisplayedAnswers();
        presenter.selected = [];
        presenter.$view.find('.selected').removeClass('selected');
        presenter.disabled = presenter.initDisabled;
        presenter.isVisible = presenter.initIsVisible;
        presenter.updateDisability();
        presenter.updateVisibility();
        presenter.isStarted = false;
    };

    presenter.getErrorCount = function(){
        if (!presenter.activity) {
            return 0;
        }

        let errorCount = 0;
        presenter.selected.forEach((selectedIndex) =&gt; (
            presenter.answers[selectedIndex] == 0 &amp;&amp; errorCount++
        ));
        return errorCount;
    };

    presenter.getMaxScore = function(){
        if (!presenter.activity) {
            return 0;
        }

        let maxScore = 0;
        presenter.answers.forEach((answer) =&gt; maxScore += parseInt(answer));
        return maxScore;
    };

    presenter.getScore = function(){
        if (!presenter.activity) {
            return 0;
        }

        let score = 0;
        presenter.selected.forEach((selectedIndex) =&gt; (
            presenter.answers[selectedIndex] == 1 &amp;&amp; score++
        ));
        return score;
    };

    presenter.getState = function(){
        presenter.handleDisplayedAnswers();

        return JSON.stringify({
            disabled : presenter.disabled,
            visible : presenter.isVisible,
            lines : presenter.selected,
            isStarted : presenter.isStarted
        });
    };

    presenter.setState = function(state){
        presenter.isVisible = JSON.parse(state).visible;
        presenter.disabled = JSON.parse(state).disabled;
        presenter.selected = JSON.parse(state).lines;
        presenter.isStarted = JSON.parse(state).isStarted;
        for (var i = 0; i &lt; presenter.selected.length; i++) {
            presenter.$view.find('#line_'+presenter.selected[i]).attr('class','line selected');
        }
        presenter.updateDisability();
        presenter.updateVisibility();
    };

    presenter.handleDisplayedAnswers = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
    };

    presenter.isAnswersDisplayed = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    };

    presenter.getActivitiesCount = function () {
        return !presenter.activity ? 0 : presenter.answers.filter(value =&gt; value == 1).length;
    };

    return presenter;
}
</presenter></addon><addon id="Logger">
	<model>
	</model>
<css>.logger {
    text-align: center;
    font-weight: bold;
    border: 1px dashed #999999;
    padding: 0;
    color: red;
    overflow: auto;
    width: 100%;
    height: 100%;
}

.log-element {
    color: black;
    border-top: 1px dotted black;
    font-weight: normal;
}

.logger-clear {
    float: right;
}
</css><view>&lt;div class="logger"&gt;Logger - logs interceptor &lt;a href="" class="logger-clear"&gt;[Clear]&lt;/a&gt;&lt;/div&gt;
</view><preview>&lt;div class="logger"&gt;Logger - logs interceptor &lt;a href="" class="logger-clear"&gt;[Clear]&lt;/a&gt;&lt;/div&gt;
</preview><presenter>function AddonLogger_create() {
    var presenter = function () {};

    presenter.run = function(view, model) {
        presenter.$view = $(view);

        jQuery.aop.around({ target:console, method:'log'}, function (invocation) {
            insertLogElement(invocation.arguments[0]);
            return invocation.proceed();
        });

        presenter.$view.find('.logger-clear').click(function () {
            presenter.$view.find('.log-element').remove();
            return false;
        });
    };

    function insertLogElement(logMessage) {
        var $element = $(document.createElement('div')), logDate = new Date(), formattedDate;
        formattedDate = logDate.getHours() + ":" + logDate.getMinutes() + ":" + logDate.getSeconds();

        $element.addClass('log-element');
        $element.text(formattedDate + ": " + logMessage);

        presenter.$view.find('.logger').append($element);
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="LottiePlayer" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property isDefault="true" name="Items" nameLabel="LottiePlayer_property_items" type="list">
            <property name="AnimationJSON" nameLabel="LottiePlayer_property_animation_json" type="file"/>
            <property name="Loop" nameLabel="LottiePlayer_property_loop" type="boolean"/>
            <property name="LoopsNumber" nameLabel="LottiePlayer_property_loops_number" type="int"/>
            <property name="Autoplay" nameLabel="LottiePlayer_property_autoplay" type="boolean"/>
            <property name="Direction" nameLabel="LottiePlayer_property_direction" type="{Forward,Backward}"/>
            <property name="Mode" nameLabel="LottiePlayer_property_mode" type="{Normal,Bounce}"/>
            <property name="Speed" nameLabel="LottiePlayer_property_speed" type="number"/>
            <property name="Intermission" nameLabel="LottiePlayer_property_intermission_ms" type="int"/>
            <property name="Background" nameLabel="LottiePlayer_property_background" type="string"/>
            <property name="AlternativeText" nameLabel="LottiePlayer_property_alternative_text" type="text"/>
            <property name="PreviewAlternativeText" nameLabel="LottiePlayer_property_preview_alternative_text" type="text"/>
        </property>
        <property name="Controls" nameLabel="LottiePlayer_property_controls" type="boolean"/>
        <property name="PlayInSuccession" nameLabel="LottiePlayer_property_play_in_succession" type="boolean"/>
        <property name="LoopSuccession" nameLabel="LottiePlayer_property_loop_succession" type="boolean"/>
        <property name="SendEventOnEveryFrame" nameLabel="LottiePlayer_property_send_event_on_every_frame" type="boolean"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="LottiePlayer_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="LottiePlayer_property_speech_texts" type="staticlist">
            <property name="PlayButton" nameLabel="LottiePlayer_property_speech_text_play_button" type="staticrow">
                <property name="PlayButton" nameLabel="LottiePlayer_property_speech_text_play_button" type="string"/>
            </property>
            <property name="PauseButton" nameLabel="LottiePlayer_property_speech_text_pause_button" type="staticrow">
                <property name="PauseButton" nameLabel="LottiePlayer_property_speech_text_pause_button" type="string"/>
            </property>
            <property name="StopButton" nameLabel="LottiePlayer_property_speech_text_stop_button" type="staticrow">
                <property name="StopButton" nameLabel="LottiePlayer_property_speech_text_stop_button" type="string"/>
            </property>
            <property name="LoopButton" nameLabel="LottiePlayer_property_speech_text_loop_button" type="staticrow">
                <property name="LoopButton" nameLabel="LottiePlayer_property_speech_text_loop_button" type="string"/>
            </property>
            <property name="Paused" nameLabel="LottiePlayer_property_speech_text_paused" type="staticrow">
                <property name="Paused" nameLabel="LottiePlayer_property_speech_text_paused" type="string"/>
            </property>
            <property name="Stopped" nameLabel="LottiePlayer_property_speech_text_stopped" type="staticrow">
                <property name="Stopped" nameLabel="LottiePlayer_property_speech_text_stopped" type="string"/>
            </property>
        </property>
        <property name="{library}" nameLabel="LottiePlayer_property_library" type="script" value="/file/serve/4732428471631872"/>
    </model>
<css>.lottie-player-invalid-configuration {
    border: 3px solid red !important;
    color: red;
    line-height: 18px;
    font-size: 18px;
    display: flex;
    justify-content: center;
    align-items: center;
    text-align: center;
    padding: 2px
}

.lottie-player-visible-animation {
    display: flex;
}

.lottie-player-invisible-animation {
    display: none;
}

lottie-player::part(keyboard_navigation_active_element)  {
    border: 1px dotted grey;
}
</css><view/><preview/><presenter>function AddonLottiePlayer_create() {
    var presenter = function () {};

    presenter.eventBus = null;

    presenter.isVisible = true;
    presenter.currentAnimationIndex = 0;
    presenter.animationsStates = [];
    presenter.animationsElements = [];
    presenter.isPreview = true;
    presenter.previouslyVisited = false;

    presenter.isWCAGOn = false;
    presenter.speechTexts = null;
    presenter.keyboardControllerObject = null;

    presenter.DIRECTION = {
        Forward: "Forward",
        Backward: "Backward",
        DEFAULT: "Forward"
    };

    presenter.MODE = {
        Normal: "Normal",
        Bounce: "Bounce",
        DEFAULT: "Normal"
    };

    presenter.ERROR_CODES = {
        IAS_0: "Please provide animation JSON file.",
        ILN_0: "Number of loops must be valid integer or empty.",
        ILN_1: "Number of loops must not be less than 0.",
        IS_0: "Speed value must be valid float or empty.",
        IS_1: "Speed value must not be less than 0.",
        II_0: "Intermission must be valid integer or empty.",
        II_1: "Intermission value must not be less than 0.",
        IB_0: "Background color must be valid HEX value, RGB value, RGBA value, transparent or empty (to be transparent)",
    };

    presenter.CSS_CLASSES = {
        INVALID_CONFIGURATION: "lottie-player-invalid-configuration",
        VISIBLE_ANIMATION: "lottie-player-visible-animation",
        INVISIBLE_ANIMATION: "lottie-player-invisible-animation",
        LOTTIE_PLAYER: "lottie-player",
    };

    presenter.DOM_IDS = {
        PLAY_BUTTON: "lottie-play-button",
        STOP_BUTTON: "lottie-stop-button",
        LOOP_BUTTON: "lottie-loop-toggle",
    };

    presenter.DEFAULT_TTS_PHRASES = {
        PLAY_BUTTON: "Play button",
        PAUSE_BUTTON: "Pause button",
        STOP_BUTTON: "Stop button",
        LOOP_BUTTON: "Loop button",
        STOPPED: "Stopped",
        PAUSED: "Paused"
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.initialize = function (view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.isPreview = isPreview;

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            showErrorMessage();
            return;
        }

        presenter.addAnimations(view, presenter.configuration);
        presenter.attachEventListeners(isPreview);

        if (!isPreview) {
            presenter.setSpeechTexts(model["SpeechTexts"]);
            presenter.buildKeyboardController();
        }
        presenter.displayCurrentAnimation();
    };

    presenter.validateModel = function(model) {
        let isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        let controls = ModelValidationUtils.validateBoolean(model["Controls"]);
        let playInSuccession = ModelValidationUtils.validateBoolean(model["PlayInSuccession"]);
        let loopSuccession = ModelValidationUtils.validateBoolean(model["LoopSuccession"]);
        let sendEventOnEveryFrame = ModelValidationUtils.validateBoolean(model["SendEventOnEveryFrame"]);

        let animationsConfigurations = [];
        for (let itemIndex = 0; itemIndex &lt; model.Items.length; itemIndex++) {
            let validatedAnimationConfig = validateAnimationModel(model.Items[itemIndex], itemIndex);
            if (!validatedAnimationConfig.isValid) {
                return validatedAnimationConfig;
            }
            animationsConfigurations.push(validatedAnimationConfig.configuration);
        }

        return {
            isValid: true,
            addonID: model["ID"],
            isVisible: isVisible,
            animations: animationsConfigurations,
            controls: controls,
            playInSuccession: playInSuccession,
            loopSuccession: loopSuccession,
            sendEventOnEveryFrame: sendEventOnEveryFrame,
            langTag: model["langAttribute"],
        };
    };

    function validateAnimationModel(animationModel, itemIndex) {
        let validatedAnimationSrc = validateAnimationSrc(animationModel["AnimationJSON"], itemIndex);
        if (!validatedAnimationSrc.isValid) {
            return validatedAnimationSrc;
        }

        let isLoop = ModelValidationUtils.validateBoolean(animationModel["Loop"]);
        let isAutoplay = ModelValidationUtils.validateBoolean(animationModel["Autoplay"]);
        let direction = ModelValidationUtils.validateOption(presenter.DIRECTION, animationModel["Direction"]);
        let mode = ModelValidationUtils.validateOption(presenter.MODE, animationModel["Mode"]);

        let validatedLoopsNumber = validateLoopsNumber(animationModel["LoopsNumber"], itemIndex);
        if (!validatedLoopsNumber.isValid) {
            return validatedLoopsNumber;
        }

        let validatedSpeed = validateSpeed(animationModel["Speed"], itemIndex);
        if (!validatedSpeed.isValid) {
            return validatedSpeed;
        }

        let validatedIntermission = validateIntermission(animationModel["Intermission"], itemIndex);
        if (!validatedIntermission.isValid) {
            return validatedIntermission;
        }

        let validatedBackground = validateBackground(animationModel["Background"], itemIndex);
        if (!validatedBackground.isValid) {
            return validatedBackground;
        }

        return {
            isValid: true,
            configuration: {
                animationSrc: validatedAnimationSrc.value,
                isLoop: isLoop,
                isAutoplay: isAutoplay,
                direction: direction,
                mode: mode,
                loopsNumber: validatedLoopsNumber.value,
                speed: validatedSpeed.value,
                intermission: validatedIntermission.value,
                background: validatedBackground.value,
                altText: animationModel["AlternativeText"],
                altTextPreview: animationModel["PreviewAlternativeText"],
            },
        };
    }

    function validateAnimationSrc (value, itemIndex) {
        if (!value) {
            return getErrorObject("IAS_0", itemIndex);
        }
        return getValidObject(value);
    }

    function validateLoopsNumber (value, itemIndex) {
        return validatePositiveIntValue("ILN", value, null, itemIndex);
    }

    function validateSpeed (value, itemIndex) {
        return validatePositiveFloatValue("IS", value, 1, itemIndex);
    }

    function validateIntermission (value, itemIndex) {
        return validatePositiveIntValue("II", value, null, itemIndex);
    }

    function validateBackground (value, itemIndex) {
        let defaultValue = "transparent";
        if (ModelValidationUtils.isStringEmpty(value)) {
            return getValidObject(defaultValue);
        }

        let parsedValue = value.toLowerCase();
        if (parsedValue === defaultValue) {
            return getValidObject(parsedValue);
        }

        // Regex do not support RGBA with spaces
        let isColor = value.match(/#[a-f0-9]{6}\b|#[a-f0-9]{3}\b|rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$|rgba\((\s*\d+\s*,){3}[\d\.]+\)/gi);
        if (!isColor) {
            return getErrorObject("IB_0", itemIndex);
        }
        return getValidObject(parsedValue);
    }

    function validatePositiveFloatValue(errorCodePrefix, value, defaultValue, itemIndex) {
        if (ModelValidationUtils.isStringEmpty(value)) {
            return getValidObject(defaultValue);
        }

        const parsedValue = parseFloat(value);
        if (isNaN(parsedValue)) {
            return getErrorObject(`${errorCodePrefix}_0`, itemIndex);
        }
        if (parsedValue &lt; 0) {
            return getErrorObject(`${errorCodePrefix}_1`, itemIndex);
        }

        return getValidObject(parsedValue);
    }

    function validatePositiveIntValue(errorCodePrefix, value, defaultValue, itemIndex) {
        if (ModelValidationUtils.isStringEmpty(value)) {
            return getValidObject(defaultValue);
        }

        const parsedValue = parseInt(value);
        if (isNaN(parsedValue)) {
            return getErrorObject(`${errorCodePrefix}_0`, itemIndex);
        }
        if (parsedValue &lt; 0) {
            return getErrorObject(`${errorCodePrefix}_1`, itemIndex);
        }

        return getValidObject(parsedValue);
    }

    function getValidObject (value) {
        return {
            isValid: true,
            value: value
        }
    }

    function getErrorObject(errorCode, itemIndex = undefined) {
        return {
            isValid: false,
            errorCode: errorCode,
            itemIndex: itemIndex
        }
    }

    function showErrorMessage() {
        let errorMessage = presenter.ERROR_CODES[presenter.configuration.errorCode];
        if (presenter.configuration.itemIndex !== undefined) {
            errorMessage = `Item ${presenter.configuration.itemIndex + 1}. ` + errorMessage;
        }
        presenter.$view.html(errorMessage);
        presenter.view.classList.add(presenter.CSS_CLASSES.INVALID_CONFIGURATION);
    }

    presenter.addAnimations = function (view, configuration) {
        configuration.animations.forEach((animationConfiguration, index) =&gt; {
            let lottie = document.createElement("lottie-player");
            hideAnimation(lottie);
            lottie.classList.add(`${presenter.CSS_CLASSES.LOTTIE_PLAYER}-${index}`);

            lottie.src = animationConfiguration.animationSrc;
            lottie.background = animationConfiguration.background;

            lottie.setAttribute("direction", parseDirectionToValue(animationConfiguration.direction));
            lottie.setAttribute("speed", animationConfiguration.speed.toString());

            if (animationConfiguration.isLoop) {
                lottie.setAttribute("loop", "loop");
            }
            if (animationConfiguration.isAutoplay) {
                lottie.setAttribute("autoplay", "autoplay");
            }
            if (configuration.controls) {
                lottie.setAttribute("controls", "controls");
            }
            if (animationConfiguration.mode === presenter.MODE.Bounce) {
                lottie.setAttribute("mode", "bounce");
            }
            if (animationConfiguration.loopsNumber !== null) {
                lottie.setAttribute("count", animationConfiguration.loopsNumber.toString());
            }
            if (animationConfiguration.intermission !== null) {
                lottie.setAttribute("intermission", animationConfiguration.intermission.toString());
            }

            view.appendChild(lottie);
        });

        presenter.animationsElements = findAnimationsElements();
        presenter.initializeAnimationsStates();
    };

    presenter.initializeAnimationsStates = function () {
        presenter.animationsElements.forEach((animation, index) =&gt; {
            presenter.animationsStates.push(new AnimationState(index));
        })
    }

    function parseDirectionToValue (mode) {
        if (mode === presenter.DIRECTION.Backward) {
            return "-1";
        } return "1";
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            PlayButton: presenter.DEFAULT_TTS_PHRASES.PLAY_BUTTON,
            PauseButton: presenter.DEFAULT_TTS_PHRASES.PAUSE_BUTTON,
            StopButton: presenter.DEFAULT_TTS_PHRASES.STOP_BUTTON,
            LoopButton: presenter.DEFAULT_TTS_PHRASES.LOOP_BUTTON,
            Paused: presenter.DEFAULT_TTS_PHRASES.PAUSED,
            Stopped: presenter.DEFAULT_TTS_PHRASES.STOPPED,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            PlayButton: TTSUtils.getSpeechTextProperty(
                speechTexts.PlayButton.PlayButton,
                presenter.speechTexts.PlayButton),
            PauseButton: TTSUtils.getSpeechTextProperty(
                speechTexts.PauseButton.PauseButton,
                presenter.speechTexts.PauseButton),
            StopButton: TTSUtils.getSpeechTextProperty(
                speechTexts.StopButton.StopButton,
                presenter.speechTexts.StopButton),
            LoopButton: TTSUtils.getSpeechTextProperty(
                speechTexts.LoopButton.LoopButton,
                presenter.speechTexts.LoopButton),
            Paused: TTSUtils.getSpeechTextProperty(
                speechTexts.Paused.Paused,
                presenter.speechTexts.Paused),
            Stopped: TTSUtils.getSpeechTextProperty(
                speechTexts.Stopped.Stopped,
                presenter.speechTexts.Stopped),
        };
    };

    presenter.executeCommand = function (name, params) {
        let commands = {
            load: presenter.load,
            play: presenter.play,
            pause: presenter.pause,
            stop: presenter.stop,
            freeze: presenter.freeze,
            playAll: presenter.playAll,
            pauseAll: presenter.pauseAll,
            stopAll: presenter.stopAll,
            freezeAll: presenter.freezeAll,
            loop: presenter.loop,
            frame: presenter.frame,
            jumpTo: presenter.jumpToCommand,
            next: presenter.next,
            previous: presenter.previous,
            show: presenter.show,
            hide: presenter.hide,
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.load = function (animationIndex, url) {
        presenter.animationsElements[animationIndex - 1].load(url);
    };

    presenter.play = function (animationIndex) {
        const parsedAnimationIndex = animationIndex ? animationIndex - 1 : presenter.currentAnimationIndex;
        play(parsedAnimationIndex);
    };

    function play (animationIndex) {
        presenter.animationsElements.forEach((animation, index) =&gt; {
            if (index === animationIndex) {
                presenter.currentAnimationIndex = index;
                showAnimation(animation);
                animation.play();
            } else {
                animation.stop();
                hideAnimation(animation);
            }
        })
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakAlternativeText();
        }
    }

    presenter.pause = function (animationIndex) {
        const parsedAnimationIndex = animationIndex ? animationIndex - 1 : presenter.currentAnimationIndex;
        pause(parsedAnimationIndex);
    };

    function pause (animationIndex) {
        presenter.animationsElements[animationIndex].pause();
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakPaused();
        }
    }

    presenter.stop = function (animationIndex) {
        const parsedAnimationIndex = animationIndex ? animationIndex - 1 : presenter.currentAnimationIndex;
        stop(parsedAnimationIndex);
    };

    function stop (animationIndex) {
        presenter.animationsElements[animationIndex].stop();
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakStopped();
        }
    }

    presenter.freeze = function (animationIndex) {
        const parsedAnimationIndex = animationIndex ? animationIndex - 1 : presenter.currentAnimationIndex;
        freeze(parsedAnimationIndex);
    };

    function freeze (animationIndex) {
        presenter.animationsElements[animationIndex].freeze();
    }

    presenter.playAll = function () {
        presenter.play()
    };

    presenter.pauseAll = function () {
        presenter.animationsElements.forEach(animation =&gt; {
            animation.pause();
        })
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakPaused();
        }
    };

    presenter.stopAll = function () {
        presenter.animationsElements.forEach(animation =&gt; {
            animation.stop();
        })
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakStopped();
        }
    };

    presenter.freezeAll = function () {
        presenter.animationsElements.forEach(animation =&gt; {
            animation.freeze();
        })
    };

    presenter.loop = function (id, value) {
        presenter.animationsElements[id - 1].setLooping(value);
    };

    presenter.frame = function (id, value) {
        presenter.animationsElements[id - 1].seek(value);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.isVisible = isVisible;
    };

    presenter.next = function () {
        if (presenter.currentAnimationIndex + 1 &lt; presenter.configuration.animations.length) {
            presenter.currentAnimationIndex++;
            presenter.jumpTo(presenter.currentAnimationIndex + 1);
        }
    };

    presenter.previous = function () {
        if (presenter.currentAnimationIndex &gt; 0) {
            presenter.currentAnimationIndex--;
            presenter.jumpTo(presenter.currentAnimationIndex + 1);
        }
    };

    presenter.jumpToCommand = function (params) {
        presenter.jumpTo(parseInt(params[0], 10));
    };

    presenter.jumpTo = function (animationIndex) {
        let newAnimationIndex = animationIndex - 1;
        if (newAnimationIndex &gt;= 0 &amp;&amp; newAnimationIndex &lt; presenter.configuration.animations.length) {
            presenter.currentAnimationIndex = newAnimationIndex;
            if (isAnimationAutoplay(newAnimationIndex) &amp;&amp; !isAnimationFinishedLoops(newAnimationIndex)) {
                play(presenter.currentAnimationIndex);
            } else {
                presenter.displayCurrentAnimation();
            }
            if (presenter.keyboardControllerObject) {
                presenter.keyboardControllerObject.reloadElements();
            }
        }
    };

    presenter.attachEventListeners = function (isPreview) {
        if (!isPreview) {
            attachEventListenersToView();
        }
        presenter.animationsElements.forEach(
            (animation, index) =&gt; attachRunEventListenersToAnimationElement(animation, index, isPreview)
        );
    };

    function attachEventListenersToView () {
        presenter.view.addEventListener("touchstart", (e) =&gt; {
            e.stopPropagation();
        });
        presenter.view.addEventListener("touchend", presenter.clickHandler);
        presenter.view.addEventListener("click", presenter.clickHandler);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    }

    presenter.clickHandler = function (event) {
        if (event !== undefined) {
            event.stopPropagation();
        }
        let eventData = presenter.createEventData(presenter.currentAnimationIndex, 1, "");
        presenter.eventBus.sendEvent("ValueChanged", eventData);
    };

    function attachRunEventListenersToAnimationElement (animation, index, isPreview) {
        if (!isPreview) {
            animation.addEventListener("load", (e) =&gt; presenter.onAnimationLoadForRun(e, index));
            animation.addEventListener("error", (e) =&gt; presenter.onAnimationErrorForRun(e, index));
            animation.addEventListener("ready", (e) =&gt; presenter.onAnimationReadyForRun(e, index));
            animation.addEventListener("play", (e) =&gt; presenter.onAnimationPlayForRun(e, index));
            animation.addEventListener("pause", (e) =&gt; presenter.onAnimationPauseForRun(e, index));
            animation.addEventListener("stop", (e) =&gt; presenter.onAnimationStopForRun(e, index));
            animation.addEventListener("freeze", (e) =&gt; presenter.onAnimationFreezeForRun(e, index));
            animation.addEventListener("loop", (e) =&gt; presenter.onAnimationLoopForRun(e, index));
            animation.addEventListener("complete", (e) =&gt; presenter.onAnimationCompleteForRun(e, index));

            if (presenter.configuration.sendEventOnEveryFrame) {
                animation.addEventListener("frame", (e) =&gt; presenter.onAnimationFrameForRun(e, index));
            }
        }

        if (presenter.configuration.playInSuccession) {
            animation.addEventListener("ready", (e) =&gt; presenter.onAnimationReadyForPlayedInSuccession(e, index));
            animation.addEventListener("loop", (e) =&gt; presenter.onAnimationLoopForPlayedInSuccession(e, index));
            animation.addEventListener("complete", (e) =&gt; presenter.onAnimationCompleteForPlayedInSuccession(e, index));
        } else {
            animation.addEventListener("loop", (e) =&gt; presenter.onAnimationLoopForNotPlayedInSuccession(e, index));
        }
    }

    presenter.onAnimationLoadForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "load");
    };

    presenter.onAnimationErrorForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "error");
    };

    presenter.onAnimationReadyForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "ready");

        if (!presenter.previouslyVisited
            &amp;&amp; index !== presenter.currentAnimationIndex
            &amp;&amp; isAnimationAutoplay(index)) {
            presenter.animationsElements[index].stop();
        }
    };

    presenter.onAnimationReadyForPlayedInSuccession = function (event, index) {
        if (!presenter.previouslyVisited
            &amp;&amp; index === presenter.currentAnimationIndex
            &amp;&amp; !presenter.configuration.animations[index].isAutoplay) {
            presenter.animationsElements[index].play();
        }
    };

    presenter.onAnimationReadyForSetState = function (event, index, currentFrame) {
        setCurrentFrameForAnimation(index, currentFrame);

        if (isAnimationStopped(index)) {
            presenter.animationsElements[index].stop()
        } else if (isAnimationPaused(index)) {
            presenter.animationsElements[index].pause();
        } else if (isAnimationFrozen(index)) {
            presenter.animationsElements[index].freeze();
        } else if (isAnimationPlaying(index)) {
            presenter.animationsElements[index].play();
        }
    }

    function setCurrentFrameForAnimation(animationIndex, currentFrame) {
        if (currentFrame) {
            presenter.animationsElements[animationIndex].seek(Math.floor(currentFrame));
        }
    }

    presenter.onAnimationPlayForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "play");
        setAnimationStateAsPlaying(index);
    };

    presenter.onAnimationPauseForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "pause");
        setAnimationStateAsPaused(index);
    };

    presenter.onAnimationFreezeForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "freeze");
        setAnimationStateAsFrozen(index);
    };

    presenter.onAnimationStopForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "stop");
        setAnimationStateAsStopped(index);
    };

    presenter.onAnimationFrameForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "frame");
    };

    presenter.onAnimationCompleteForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "complete");

        if (presenter.configuration.animations[index].isLoop &amp;&amp; presenter.configuration.animations[index].loopsNumber) {
            increaseAnimationPlayedLoops(index);
        }
    };

    presenter.onAnimationCompleteForPlayedInSuccession = function (event, index) {
        jumpToNextAnimationInSuccession();
    };

    presenter.onAnimationLoopForRun = function (event, index) {
        presenter.sendOnAnimationChangeEvent(index, "loop");
    };

    presenter.onAnimationLoopForPlayedInSuccession = function (event, index) {
        if (!isAnimationFinishedLoops(index)) {
            increaseAnimationPlayedLoops(index);
        } else {
            resetAnimationPlayedLoops(index);
            jumpToNextAnimationInSuccession();
        }
    };

    presenter.onAnimationLoopForNotPlayedInSuccession = function (event, index) {
        if (!isAnimationFinishedLoops(index)) {
            increaseAnimationPlayedLoops(index);
        }
    };

    function isAnimationAutoplay (animationIndex) {
        return presenter.configuration.animations[animationIndex].isAutoplay || presenter.configuration.playInSuccession;
    }

    function jumpToNextAnimationInSuccession() {
        presenter.animationsElements[presenter.currentAnimationIndex].stop();

        let nextIndex = presenter.currentAnimationIndex + 1;
        if (nextIndex &gt;= presenter.animationsElements.length) {
            if (!presenter.configuration.loopSuccession) return;
            nextIndex = 0;
        }

        hideAnimationWithIndex(presenter.currentAnimationIndex);
        presenter.currentAnimationIndex = nextIndex;
        showAnimationWithIndex(presenter.currentAnimationIndex);
        if (presenter.keyboardControllerObject) {
            presenter.keyboardControllerObject.reloadElements();
        }
        presenter.animationsElements[presenter.currentAnimationIndex].play();
        if (presenter.isTTS() &amp;&amp; !presenter.keyboardControllerObject.isReadInSuccession[presenter.currentAnimationIndex]){
            presenter.keyboardControllerObject.isReadInSuccession[presenter.currentAnimationIndex] = true;
            presenter.keyboardControllerObject.speakEnterAction();
        }
    }

    presenter.sendOnAnimationChangeEvent = function (item, value) {
        const eventData = presenter.createEventData(item, value);
        presenter.eventBus.sendEvent("ValueChanged", eventData);
    };

    presenter.createEventData = function (item, value) {
        return {
            source: "" + presenter.configuration.addonID,
            item: "" + item,
            value: "" + value,
            score: "",
        };
    };

    presenter.getState = function () {
        let animationsStates = [];
        presenter.animationsStates.forEach(animationState =&gt;
            animationsStates.push(animationState.serialize())
        )

        return JSON.stringify({
            isVisible: presenter.isVisible,
            currentAnimationIndex: presenter.currentAnimationIndex,
            animationsStates: presenter.animationsStates,
            animationsSavedFrames: getCurrentAnimationsFrames(),
            previouslyVisited: true,
        });
    };

    function getCurrentAnimationsFrames () {
        let currentFrames = [];
        presenter.animationsElements.forEach(animationElement =&gt;  {
            if (isAnimationPlayerDOMCreated(animationElement)) {
                currentFrames.push(animationElement.getLottie().currentFrame);
            }
        })
        return currentFrames;
    }

    function isAnimationPlayerDOMCreated(animationElement) {
        return animationElement.shadowRoot &amp;&amp; animationElement.shadowRoot.childNodes &amp;&amp; animationElement.shadowRoot.childNodes[2];
    }

    presenter.setState = function (state) {
        let parsedState = JSON.parse(state);
        presenter.setVisibility(parsedState.isVisible);
        presenter.previouslyVisited = parsedState.previouslyVisited;
        presenter.currentAnimationIndex = parsedState.currentAnimationIndex;

        presenter.animationsStates = new Array(parsedState.animationsStates.length).fill(null);
        parsedState.animationsStates.forEach((savedAnimationState, index) =&gt; {
            let animationState = new AnimationState(index);
            animationState.deserialize(savedAnimationState);
            presenter.animationsStates[animationState.index] = animationState;
        })

        presenter.animationsElements = findAnimationsElements();
        presenter.displayCurrentAnimation();

        presenter.animationsElements.forEach((animationElement, index) =&gt; {
            animationElement.addEventListener("ready",
                (e) =&gt; presenter.onAnimationReadyForSetState(e, index, parsedState.animationsSavedFrames[index]))
        })
    };

    presenter.displayCurrentAnimation = function () {
        presenter.animationsElements.forEach((element, index) =&gt; {
            if (index === presenter.currentAnimationIndex) {
                showAnimation(element);
            } else {
                hideAnimation(element);
            }
        })
    };

    function findAnimationsElements () {
        return presenter.view.querySelectorAll("lottie-player");
    }

    function hideAnimationWithIndex (animationIndex) {
        hideAnimation(presenter.animationsElements[animationIndex]);
    }

    function hideAnimation (animationElement) {
        if (animationElement.classList.contains(presenter.CSS_CLASSES.VISIBLE_ANIMATION)) {
            animationElement.classList.remove(presenter.CSS_CLASSES.VISIBLE_ANIMATION);
        }
        animationElement.classList.add(presenter.CSS_CLASSES.INVISIBLE_ANIMATION);
    }

    function showAnimationWithIndex (animationIndex) {
        showAnimation(presenter.animationsElements[animationIndex]);
    }

    function showAnimation (animationElement) {
        if (animationElement.classList.contains(presenter.CSS_CLASSES.INVISIBLE_ANIMATION)) {
            animationElement.classList.remove(presenter.CSS_CLASSES.INVISIBLE_ANIMATION);
        }
        animationElement.classList.add(presenter.CSS_CLASSES.VISIBLE_ANIMATION);
    }

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new LottiePlayerKeyboardController([], 1);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        let currentAnimation = presenter.$view.find(`.${presenter.CSS_CLASSES.VISIBLE_ANIMATION}`);
        let animations = $(currentAnimation[0].shadowRoot.childNodes[2].children[1]).find(`
            #${presenter.DOM_IDS.PLAY_BUTTON},
            #${presenter.DOM_IDS.STOP_BUTTON},
            #${presenter.DOM_IDS.LOOP_BUTTON}
        `);
        return animations
    };

    function LottiePlayerKeyboardController(elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
        this.isReadInSuccession = new Array(presenter.animationsElements.length).fill(false);
    }

    presenter.isTTS = function () {
        return presenter.isWCAGOn &amp;&amp; presenter.getTextToSpeechOrNull(presenter.playerController);
    };

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    LottiePlayerKeyboardController.prototype = Object.create(KeyboardController.prototype);
    LottiePlayerKeyboardController.prototype.constructor = LottiePlayerKeyboardController;

    LottiePlayerKeyboardController.prototype.reloadElements = function () {
        let elements = presenter.getElementsForKeyboardNavigation();

        for (let i = 0; i &lt; this.keyboardNavigationElementsLen; i++) {
            this.unmark(this.keyboardNavigationElements[i]);
        }
        this.removeEventListeners();

        this.keyboardNavigationElements = elements;
        this.keyboardNavigationElementsLen = elements.length;
        this.attachEventListeners();

        if (!this.keyboardNavigationActive) {
            this.keyboardNavigationCurrentElementIndex = 0;
            this.keyboardNavigationCurrentElement = this.keyboardNavigationElements[0];
            return;
        }

        this.keyboardNavigationCurrentElement = this.keyboardNavigationElements[this.keyboardNavigationCurrentElementIndex];
        this.mark(this.keyboardNavigationCurrentElement)
    };

    LottiePlayerKeyboardController.prototype.attachEventListeners = function () {
        for (let i = 0; i &lt; this.keyboardNavigationElementsLen; i++) {
            let element = this.keyboardNavigationElements[i];
            let $element = $(element);
            if (hasId($element, presenter.DOM_IDS.PLAY_BUTTON)) {
                element.addEventListener("click", this.onPlayButtonClick);
            } else if (hasId($element, presenter.DOM_IDS.STOP_BUTTON)) {
                element.addEventListener("click", this.onStopButtonClick);
            }
        }
    };

    LottiePlayerKeyboardController.prototype.removeEventListeners = function () {
        for (let i = 0; i &lt; this.keyboardNavigationElementsLen; i++) {
            let element = this.keyboardNavigationElements[i];
            let $element = $(element);
            if (hasId($element, presenter.DOM_IDS.PLAY_BUTTON)) {
                element.removeEventListener("click", this.onPlayButtonClick, false);
            } else if (hasId($element, presenter.DOM_IDS.STOP_BUTTON)) {
                element.removeEventListener("click", this.onStopButtonClick, false);
            }
        }
    };

    LottiePlayerKeyboardController.prototype.onPlayButtonClick = function (event) {
        if (presenter.isTTS()) {
            if (presenter.animationsElements[presenter.currentAnimationIndex].getLottie().isPaused === true) {
                presenter.keyboardControllerObject.speakPaused();
            } else {
                presenter.keyboardControllerObject.speakAlternativeText();
            }
        }
    };

    LottiePlayerKeyboardController.prototype.onStopButtonClick = function (event) {
        if (presenter.isTTS()) {
            presenter.keyboardControllerObject.speakStopped();
        }
    };

    LottiePlayerKeyboardController.prototype.mark = function (element) {
        if (element)
            element.setAttribute("part", "keyboard_navigation_active_element");
    };

    LottiePlayerKeyboardController.prototype.unmark = function (element) {
        if (element)
            element.removeAttribute("part");
    };

    LottiePlayerKeyboardController.prototype.nextElement = function (event) {
        KeyboardController.prototype.nextElement.call(this, event);
        this.readCurrentElement();
    };

    LottiePlayerKeyboardController.prototype.previousElement = function (event) {
        KeyboardController.prototype.previousElement.call(this, event);
        this.readCurrentElement();
    };

    LottiePlayerKeyboardController.prototype.nextRow = function (event) {
        KeyboardController.prototype.nextRow.call(this, event);
        this.readCurrentElement();
    };

    LottiePlayerKeyboardController.prototype.previousRow = function (event) {
        KeyboardController.prototype.previousRow.call(this, event);
        this.readCurrentElement();
    };

    LottiePlayerKeyboardController.prototype.enter = function (event) {
        if (!this.keyboardNavigationActive) {
            this.reloadElements();
            this.isReadInSuccession = new Array(presenter.animationsElements.length).fill(false);
            if (presenter.configuration.playInSuccession) {
                this.isReadInSuccession[presenter.currentAnimationIndex] = true;
            }
        }
        KeyboardController.prototype.enter.call(this, event);
        this.speakEnterAction();
    };

    LottiePlayerKeyboardController.prototype.readCurrentElement = function () {
        let $element = this.getCurrentElement();

        if (hasId($element, presenter.DOM_IDS.PLAY_BUTTON)) {
            if (presenter.animationsElements[presenter.currentAnimationIndex].getLottie().isPaused === true) {
                this.speakPlayButton();
            } else {
                this.speakPauseButton();
            }
        } else if (hasId($element, presenter.DOM_IDS.STOP_BUTTON)) {
            this.speakStopButton();
        } else if (hasId($element, presenter.DOM_IDS.LOOP_BUTTON)) {
            this.speakLoopButton();
        }
    };

    LottiePlayerKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    LottiePlayerKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    function hasId($element, id) {
        return $element.attr("id") === id;
    }

    LottiePlayerKeyboardController.prototype.speakEnterAction = function () {
        if (presenter.animationsElements[presenter.currentAnimationIndex].getLottie().isPaused === true){
            this.speakPreviewAlternativeText();
        } else {
            this.speakAlternativeText();
        }
    };

    LottiePlayerKeyboardController.prototype.speakPlayButton = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.PlayButton);
    };

    LottiePlayerKeyboardController.prototype.speakPauseButton = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.PauseButton);
    };

    LottiePlayerKeyboardController.prototype.speakStopButton = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.StopButton);
    };

    LottiePlayerKeyboardController.prototype.speakLoopButton = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.LoopButton);
    };

    LottiePlayerKeyboardController.prototype.speakStopped = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.Stopped);
    };

    LottiePlayerKeyboardController.prototype.speakPaused = function () {
        this.speakWithLanguageFromLesson(presenter.speechTexts.Paused);
    };

    LottiePlayerKeyboardController.prototype.speakAlternativeText = function () {
        this.speakWithLanguageFromPresenter(presenter.configuration.animations[presenter.currentAnimationIndex].altText);
    };

    LottiePlayerKeyboardController.prototype.speakPreviewAlternativeText = function () {
        this.speakWithLanguageFromPresenter(presenter.configuration.animations[presenter.currentAnimationIndex].altTextPreview);
    };

    LottiePlayerKeyboardController.prototype.speakWithLanguageFromLesson = function (message) {
        this.speakMessage(message, false);
    };

    LottiePlayerKeyboardController.prototype.speakWithLanguageFromPresenter = function (message) {
        this.speakMessage(message, true);
    };

    LottiePlayerKeyboardController.prototype.speakMessage = function (message, isLangFromPresenter = true) {
        let textVoices = [];
        if (isLangFromPresenter) {
            textVoices.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
        } else {
            textVoices.push(window.TTSUtils.getTextVoiceObject(message));
        }
        presenter.speak(textVoices);
    };

    presenter.speak = function (data) {
        let tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.destroy = function () {
        presenter.removeEventListeners(presenter.isPreview);

        if (presenter.playerController &amp;&amp; !presenter.playerController.isPlayerInCrossDomain()) {
            $(window.parent.document).unbind("scroll");
        }
    };

    presenter.removeEventListeners = function (isPreview) {
        if (!isPreview) {
            removeEventListenersToView();
        }
        presenter.animationsElements.forEach(
            (animation, index) =&gt; removeRunEventListenersToAnimationElement(animation, index, isPreview)
        );
        presenter.keyboardControllerObject.removeEventListeners();
    };

    function removeEventListenersToView () {
        presenter.view.removeEventListener("touchstart", (e) =&gt; {
            e.stopPropagation();
        }, false);
        presenter.view.removeEventListener("touchend", presenter.clickHandler, false);
        presenter.view.removeEventListener("click", presenter.clickHandler, false);
    }

    function removeRunEventListenersToAnimationElement (animation, index, isPreview) {
        if (!isPreview) {
            animation.removeEventListener("load", (e) =&gt; presenter.onAnimationLoadForRun(e, index), false);
            animation.removeEventListener("error", (e) =&gt; presenter.onAnimationErrorForRun(e, index), false);
            animation.removeEventListener("ready", (e) =&gt; presenter.onAnimationReadyForRun(e, index), false);
            animation.removeEventListener("play", (e) =&gt; presenter.onAnimationPlayForRun(e, index), false);
            animation.removeEventListener("pause", (e) =&gt; presenter.onAnimationPauseForRun(e, index), false);
            animation.removeEventListener("stop", (e) =&gt; presenter.onAnimationStopForRun(e, index), false);
            animation.removeEventListener("freeze", (e) =&gt; presenter.onAnimationFreezeForRun(e, index), false);
            animation.removeEventListener("loop", (e) =&gt; presenter.onAnimationLoopForRun(e, index), false);
            animation.removeEventListener("complete", (e) =&gt; presenter.onAnimationCompleteForRun(e, index), false);

            if (presenter.configuration.sendEventOnEveryFrame) {
                animation.removeEventListener("frame", (e) =&gt; presenter.onAnimationFrameForRun(e, index), false);
            }
        }

        if (presenter.configuration.playInSuccession) {
            animation.removeEventListener("ready", (e) =&gt; presenter.onAnimationReadyForPlayedInSuccession(e, index), false);
            animation.removeEventListener("loop", (e) =&gt; presenter.onAnimationLoopForPlayedInSuccession(e, index), false);
            animation.removeEventListener("complete", (e) =&gt; presenter.onAnimationCompleteForPlayedInSuccession(e, index), false);
        } else {
            animation.removeEventListener("loop", (e) =&gt; presenter.onAnimationLoopForNotPlayedInSuccession(e, index), false);
        }
        if (presenter.previouslyVisited) {
            animation.removeEventListener("ready", (e) =&gt; presenter.onAnimationReadyForSetState(e, index), false);
        }
    }

    function setAnimationStateAsPlaying(index){
        return presenter.animationsStates[index].play();
    }

    function setAnimationStateAsStopped(index){
        return presenter.animationsStates[index].stop();
    }

    function setAnimationStateAsPaused(index){
        return presenter.animationsStates[index].pause();
    }

    function setAnimationStateAsFrozen(index){
        return presenter.animationsStates[index].freeze();
    }

    function isAnimationPlaying (index) {
        return presenter.animationsStates[index].isPlaying();
    }

    function isAnimationStopped (index) {
        return presenter.animationsStates[index].isStopped();
    }

    function isAnimationPaused (index) {
        return presenter.animationsStates[index].isPaused();
    }

    function isAnimationFrozen (index) {
        return presenter.animationsStates[index].isFrozen();
    }

    function increaseAnimationPlayedLoops (index) {
        return presenter.animationsStates[index].increasePlayedLoops()
    }

    function resetAnimationPlayedLoops (index) {
        return presenter.animationsStates[index].resetPlayedLoops()
    }

    function isAnimationFinishedLoops (index) {
        if (presenter.configuration.animations[index].loopsNumber === null) {
            return false;
        }
        return presenter.animationsStates[index].playedLoops === presenter.configuration.animations[index].loopsNumber;
    }

    function AnimationState (index) {
        this.index = index;

        this.playedLoops = 0;
        this.status = this.STATUES.STOPPED;
    }

    AnimationState.prototype.STATUES = {
        STOPPED: 0,
        PLAYING: 1,
        PAUSED: 2,
        FROZEN: 3
    };

    AnimationState.prototype.serialize = function () {
        return {
            index: this.index,
            status: this.status,
            playedLoops: this.playedLoops
        };
    };

    AnimationState.prototype.deserialize = function (obj) {
        this.index = obj.index;
        this.status = obj.status;
        this.playedLoops = obj.playedLoops;
    };

    AnimationState.prototype.increasePlayedLoops = function () {
        this.playedLoops++;
    };

    AnimationState.prototype.resetPlayedLoops = function () {
        this.playedLoops = 0;
    };

    AnimationState.prototype.play = function () {
        this.status = this.STATUES.PLAYING;
    };

    AnimationState.prototype.stop = function () {
        this.status = this.STATUES.STOPPED;
    };

    AnimationState.prototype.pause = function () {
        this.status = this.STATUES.PAUSED;
    };

    AnimationState.prototype.freeze = function () {
        this.status = this.STATUES.FROZEN;
    };

    AnimationState.prototype.isPlaying = function () {
        return this.status === this.STATUES.PLAYING;
    };

    AnimationState.prototype.isStopped = function () {
        return this.status === this.STATUES.STOPPED;
    };

    AnimationState.prototype.isPaused = function () {
        return this.status === this.STATUES.PAUSED;
    };

    AnimationState.prototype.isFrozen = function () {
        return this.status === this.STATUES.FROZEN;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Magic_Boxes" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isLocalized="true" name="Grid" nameLabel="Magic_Boxes_property_grid" type="text"/>
        <property isLocalized="true" name="Answers" nameLabel="Magic_Boxes_property_answers" type="text"/>
        <property displayName="Check By Words" name="CheckByWords" nameLabel="Magic_Boxes_property_check_by_words" type="boolean"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Magic_Boxes_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Magic_Boxes_property_speech_texts" type="staticlist">
            <property name="Cell" nameLabel="Magic_Boxes_property_speech_text_cell" type="staticrow">
                <property name="Cell" nameLabel="Magic_Boxes_property_speech_text_cell" type="string"/>
            </property>
            <property name="Selected" nameLabel="Magic_Boxes_property_speech_text_selected" type="staticrow">
                <property name="Selected" nameLabel="Magic_Boxes_property_speech_text_selected" type="string"/>
            </property>
            <property name="Deselected" nameLabel="Magic_Boxes_property_speech_text_deselected" type="staticrow">
                <property name="Deselected" nameLabel="Magic_Boxes_property_speech_text_deselected" type="string"/>
            </property>
            <property name="Correct" nameLabel="Magic_Boxes_property_speech_text_correct" type="staticrow">
                <property name="Correct" nameLabel="Magic_Boxes_property_speech_text_correct" type="string"/>
            </property>
            <property name="Wrong" nameLabel="Magic_Boxes_property_speech_text_wrong" type="staticrow">
                <property name="Wrong" nameLabel="Magic_Boxes_property_speech_text_wrong" type="string"/>
            </property>
        </property>
    </model>
<css>.magicGridWrapper {
    width: 100%;
    height: 100%;
    border: 1px solid black;
    touch-action: none;
    -ms-touch-action: none;
}

.magicGrid {
    margin: 0;
    padding: 0;
    border: none;
    touch-action: none;
    -ms-touch-action: none;
}

.selectable-element {
    float: left;
    font-size: 2em;
    font-family: "Verdana";
    padding: 0;
    margin: 0;
    border-style: solid;
    border-color: white;
    border-width: 2px;
    text-align: center;
    color: black;
    touch-action: none;
    -ms-touch-action: none;
}

.selectable-element-wrapper {
    float: left;
    font-size: 2em;
    padding: 0;
    margin: 0;
    border-style: solid;
    border-color: black;
    border-width: 1px;
    text-align: center;
    touch-action: none;
    -ms-touch-action: none;
}

.selectable-element-selected {
    border-color: #65CDE5;
    background-color: #65CDE5;
    color: white;
}

.selectable-element-selected-correct {
    border-color: green;
}

.selectable-element-selected-uncorrect {
    border-color: red;
}

.selectable-element-show-answers {
    border-color: blue;
}
</css><view>&lt;div class="magicGridWrapper"&gt;
    &lt;div class="magicGrid"&gt;
        &lt;!-- Here goes generated grid cells--&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="magicGridWrapper"&gt;
    &lt;div class="magicGrid"&gt;
        &lt;!-- Here goes generated grid cells--&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonMagic_Boxes_create() {
    var presenter = function() { };

    var gridSelection = [];
    var goodSelections = [];
    var goodSelectionIndexes = [];
    var correctAnswers = [];
    var GSALetterCounter = 0;
    var usedGSAKeys = [];
    presenter.answerWords = {};
    presenter.GSAcorrectAnswerLocations = {};
    

    var gridContainerWrapper;
    var gridContainer;
    var playerController;
    var eventBus;
    var maxScore;

    presenter.isSelectionPossible = true;

    presenter.isWCAGOn = false;
    presenter.keyboardControllerObject = null;

    presenter.pointerStatus = {
        isDown: false,
        wasMoved: false
    };

    presenter.configuration = {
        rows: 0,
        columns: 0,
        gridElements: [],
        answers: [],
    };

    presenter.ERROR_MESSAGES = {
        COLUMNS : "Inconsistent column size. Each row has to have same number of elements!",
        ROWS : "Inconsistent row definition. Whitespaces, semicolons and commas aren't alowed in grid!",
        ANSWERS_NOT_PROVIDED : "Answers section is missing or empty!",
        GRID_NOT_PROVIDED : "Grid definition missing or empty!"
    };

    presenter.DEFAULT_TTS_PHRASES = {
        CELL: "Cell",
        SELECTED: "Selected",
        DESELECTED: "Deselected",
        CORRECT: "Correct",
        WRONG: "Wrong",
    };

    presenter.CSS_CLASSES = {
        GRID_WRAPPER: "magicGridWrapper",
        GRID: "magicGrid",
        ELEMENT_WRAPPER: "selectable-element-wrapper",
        ELEMENT: "selectable-element",
        SELECTED: "selectable-element-selected",
        CORRECT_SELECTED: "selectable-element-selected-correct",
        WRONG_SELECTED: "selectable-element-selected-uncorrect",
        SHOW_ANSWERS: "selectable-element-show-answers",
    };

    presenter.setPlayerController = function (controller) {
        playerController = controller;
    };

    function showErrorMessage(errorMessage) {
        var errorContainer = '&lt;p&gt;' + errorMessage + '&lt;/p&gt;';
        presenter.$view.html(errorContainer);
    }

    function reverseString(text) {
        var splitedText = text.split("");
        var reversedText = splitedText.reverse();

        return reversedText.join("");
    }

    function initGridSelection() {
        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            gridSelection[row] = [];
            correctAnswers[row] = [];
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                gridSelection[row][column] = false;
                correctAnswers[row][column] = false;
            }
        }
    }

    function clearCorrectAnswers() {
        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            correctAnswers[row] = [];
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                correctAnswers[row][column] = false;
            }
        }
    }

    function getElementDimensions(element) {
        element = $(element);
        
        return {
            border:{
                top:parseFloat(element.css('border-top-width'), 10),
                bottom:parseFloat(element.css('border-bottom-width'), 10),
                left:parseFloat(element.css('border-left-width'), 10),
                right:parseFloat(element.css('border-right-width'), 10)
            },
            margin:{
                top:parseInt(element.css('margin-top'), 10),
                bottom:parseInt(element.css('margin-bottom'), 10),
                left:parseInt(element.css('margin-left'), 10),
                right:parseInt(element.css('margin-right'), 10)
            },
            padding:{
                top:parseInt(element.css('padding-top'), 10),
                bottom:parseInt(element.css('padding-bottom'), 10),
                left:parseInt(element.css('padding-left'), 10),
                right:parseInt(element.css('padding-right'), 10)
            }
        };
    }

    function calculateInnerDistance(elementDimensions) {
        var vertical = elementDimensions.border.top + elementDimensions.border.bottom;
        vertical += elementDimensions.margin.top + elementDimensions.margin.bottom;
        vertical += elementDimensions.padding.top + elementDimensions.padding.top;

        var horizontal = elementDimensions.border.left + elementDimensions.border.right;
        horizontal += elementDimensions.margin.left + elementDimensions.margin.right;
        horizontal += elementDimensions.padding.left + elementDimensions.padding.right;

        return {
            vertical : vertical,
            horizontal : horizontal
        };
    }

    function fixTouch (touch) {
        var winPageX = window.pageXOffset,
            winPageY = window.pageYOffset,
            x = touch.clientX,
            y = touch.clientY;

        if (touch.pageY === 0 &amp;&amp; Math.floor(y) &gt; Math.floor(touch.pageY) ||
            touch.pageX === 0 &amp;&amp; Math.floor(x) &gt; Math.floor(touch.pageX)) {
            // iOS4 clientX/clientY have the value that should have been
            // in pageX/pageY. While pageX/page/ have the value 0
            x = x - winPageX;
            y = y - winPageY;
        } else if (y &lt; (touch.pageY - winPageY) || x &lt; (touch.pageX - winPageX) ) {
            // Some Android browsers have totally bogus values for clientX/Y
            // when scrolling/zooming a page. Detectable since clientX/clientY
            // should never be smaller than pageX/pageY minus page scroll
            x = touch.pageX - winPageX;
            y = touch.pageY - winPageY;
        }

        return {
            x: x,
            y: y
        };
    }

    function initGrid(model, preview) {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        for(var row = 0; row &lt; rows; row++) {
            for(var column = 0; column &lt; columns; column++) {
                var wrapperElement = $(document.createElement('div'));
                wrapperElement.addClass(presenter.CSS_CLASSES.ELEMENT_WRAPPER);

                var selectableElement = $(document.createElement('div'));
                selectableElement.addClass(presenter.CSS_CLASSES.ELEMENT);
                var rawChar = presenter.configuration.gridElements[row][column];
                var charToShow = isCharacterToCapitalize(rawChar) ? rawChar.toUpperCase() : rawChar;
                selectableElement.text(charToShow);

                wrapperElement.append(selectableElement);
                gridContainer.append(wrapperElement);
            }
        }

        var gridContainerWrapperDimensions = getElementDimensions(gridContainerWrapper);
        var gridContainerWrapperDistances = calculateInnerDistance(gridContainerWrapperDimensions);

        var wrapperDimensions = getElementDimensions(gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT_WRAPPER}:first`)[0]);
        var wrapperDistances = calculateInnerDistance(wrapperDimensions);

        var elementDimensions = getElementDimensions(gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:first`)[0]);
        var elementDistances = calculateInnerDistance(elementDimensions);

        var wrapperWidth = parseInt((model.Width - gridContainerWrapperDistances.horizontal - (wrapperDistances.horizontal * columns)) / columns, 10);
        var wrapperHeight = parseInt((model.Height - gridContainerWrapperDistances.vertical - (wrapperDistances.vertical * rows)) / rows, 10);

        var elementWidth = wrapperWidth - elementDistances.horizontal;
        var elementHeight = wrapperHeight - elementDistances.vertical;

        var newContainerWrapperHeight = wrapperHeight * rows + wrapperDistances.vertical * rows;
        var newContainerWrapperWidth = wrapperWidth * columns + wrapperDistances.horizontal * columns;

        var verticalGap = model.Height - newContainerWrapperHeight;
        var horizontalGap = model.Width - newContainerWrapperWidth;

        gridContainerWrapper.css('height', model.Height + 'px');
        gridContainerWrapper.css('width', model.Width + 'px');
        gridContainer.css('height', model.Height + 'px');
        gridContainer.css('width', model.Width + 'px');

        presenter._addEventListenersToGrid();

        gridContainer.find(`.${presenter.CSS_CLASSES.ELEMENT_WRAPPER}`).each(function() {
            var index = $(this).index();
            var selectedRow = parseInt(index / columns, 10);
            var selectedColumn = parseInt(index % columns, 10);

            $(this).width(wrapperWidth + horizontalGap / columns);
            $(this).height(wrapperHeight + verticalGap / rows);

            var $selectableElement = $(this).find(`.${presenter.CSS_CLASSES.ELEMENT}:first`);
            $selectableElement.width(elementWidth + horizontalGap / columns);
            $selectableElement.height(elementHeight + verticalGap / rows);
            var lineHeight = selectedRow === rows -1 ? elementHeight + verticalGap : elementHeight;
            $selectableElement.css('line-height', lineHeight + "px");

            applySelectionStyle(selectedRow, selectedColumn);
            if (!preview) {
                presenter._addEventListenersToSelectableElement($selectableElement[0], selectedRow, selectedColumn);
            }
        });
    }

    presenter._addEventListenersToGrid = function () {
        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported("touchstart")) {
            addPointingEventListenersToGrid();
        } else {
            addTouchEventListenersToGrid();
        }
    };

    function addPointingEventListenersToGrid() {
        const gridWrapper = presenter.view.getElementsByClassName(presenter.CSS_CLASSES.GRID_WRAPPER)[0];
        gridWrapper.addEventListener(EventsUtils.PointingEvents.TYPES.DOWN, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            presenter.pointerStatus.isDown = true;
        }, false);

        gridWrapper.addEventListener(EventsUtils.PointingEvents.TYPES.UP, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            presenter.pointerStatus.isDown = false;
        }, false);

        gridWrapper.addEventListener(EventsUtils.PointingEvents.TYPES.LEAVE, function (event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            presenter.pointerStatus.isDown = false;
        }, false);
    }

    function addTouchEventListenersToGrid() {
        let selectedIndex = null;
        const grid = presenter.view.getElementsByClassName(presenter.CSS_CLASSES.GRID)[0];
        grid.addEventListener("touchmove", function (event) {
            event.stopPropagation();
            event.preventDefault();

            const clientPosition = fixTouch(event.touches[0] || event.changedTouches[0]);
            const element = document.elementFromPoint(clientPosition.x, clientPosition.y);
            const index = $(element).parent().index();
            const selectedRow = parseInt(index / presenter.configuration.columns, 10);
            const selectedColumn = parseInt(index % presenter.configuration.columns, 10);
            if (index !== selectedIndex &amp;&amp; element.parentElement.classList.contains(presenter.CSS_CLASSES.ELEMENT_WRAPPER)){
                selectionHandler(selectedRow, selectedColumn);
                selectedIndex = index;
            }
        }, false);
    }

    presenter._addEventListenersToSelectableElement = function (element, row, column) {
        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported("touchstart")) {
            addPointingListenersToSelectableElement(element, row, column);
        } else {
            addTouchListenersToSelectableElement(element, row, column);
        }
    };

    function addPointingListenersToSelectableElement(element, row, column) {
        element.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, function (event) {
            event.preventDefault();
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            if (presenter.pointerStatus.isDown &amp;&amp; !presenter.pointerStatus.wasMoved){
                selectionHandler(row, column);
                presenter.pointerStatus.wasMoved = true;
            }
        }, false);

        element.addEventListener(EventsUtils.PointingEvents.TYPES.OUT, function (event) {
            event.stopPropagation();
            event.preventDefault();
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            presenter.pointerStatus.wasMoved = false;
        }, false);

        element.addEventListener(EventsUtils.PointingEvents.TYPES.DOWN, function (event) {
            event.stopPropagation();
            event.preventDefault();
            if (EventsUtils.PointingEvents.hasPointerEventSupport()) {
                element.releasePointerCapture(event.pointerId);
            }
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            presenter.pointerStatus.isDown = true;
            selectionHandler(row, column);
            presenter.pointerStatus.wasMoved = true;
        }, false);

        element.addEventListener(EventsUtils.PointingEvents.TYPES.UP, function (event) {
            event.stopPropagation();
            event.preventDefault();
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }

            presenter.pointerStatus.isDown = false;
        }, false);
    }

    function addTouchListenersToSelectableElement(element, row, column) {
        function handler(event){
            event.stopPropagation();
            event.preventDefault();
            selectionHandler(row, column);
            element.removeEventListener("click", handler);
            setTimeout(function(){
                element.addEventListener("click", handler, false);
            }, 500);
        }

        element.addEventListener("click", handler, false);
    }

    function isCharacterToCapitalize (character) {
        return !(/\u00DF/.test(character));
    }

    presenter.calculateScoreForEvent = function (prevScore, currentScore) {
        var score;
        if(currentScore &gt; prevScore){
            score = 1;
        }else{
            score = 0;
        }

        return score;
    };

    function selectionHandler(row, column) {
        if (!presenter.isSelectionPossible) {
            return;
        }

        const prevScore = presenter.getScore();

        gridSelection[row][column] = !gridSelection[row][column];
        applySelectionStyle(row, column);

        const item = (row+1) +"-"+ (column+1);
        const index = row * presenter.configuration.columns + column;
        const element = gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:eq(${index})`);
        const currentScore = presenter.getScore();

        const eventData = presenter.createEventData(item, element.text(), presenter.calculateScoreForEvent(prevScore, currentScore));
        eventBus.sendEvent("ValueChanged", eventData);

        if (presenter.isAllOK()){
            const allOKEventData = presenter.createAllOKEventData();
            eventBus.sendEvent("ValueChanged", allOKEventData);
        }
    }

    function applySelectionStyle(row, column) {
        if (row &gt;= presenter.configuration.rows || column &gt;= presenter.configuration.columns) return;
        var index = row * presenter.configuration.columns + column;
        var element = gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:eq(${index})`);

        if(gridSelection[row][column]) {// is selected
            if(!element.hasClass(presenter.CSS_CLASSES.SELECTED)) {
                element.addClass(presenter.CSS_CLASSES.SELECTED);
            }
        } else {
            if(element.hasClass(presenter.CSS_CLASSES.SELECTED)) {
                element.removeClass(presenter.CSS_CLASSES.SELECTED);
            }
        }
    }

    function applyAnswerStyles() {
        if(presenter.configuration.checkByWords){
            gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}`).each(function(index) {
                if($(this).hasClass(presenter.CSS_CLASSES.SELECTED)){
                    $(this).addClass(presenter.CSS_CLASSES.WRONG_SELECTED);
                }
            });

            for (var i=0; i&lt;presenter.configuration.answers.length; i++){
                if(presenter.checkIfWordIsSelected(presenter.configuration.answers[i].toString())){
                    var word = presenter.configuration.answers[i].toString();
                    for(var j=0; j &lt; presenter.answerWords[word.toLowerCase()].column.length; j++){
                        var index = presenter.answerWords[word.toLowerCase()].row[j] * presenter.configuration.columns + presenter.answerWords[word.toLowerCase()].column[j];
                        var element = gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:eq(${index})`);
                        element.removeClass(presenter.CSS_CLASSES.WRONG_SELECTED);
                        element.addClass(presenter.CSS_CLASSES.CORRECT_SELECTED);
                    }
                }
            }
        }else{
            gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}`).each(function(index) {
                if(!$(this).hasClass(presenter.CSS_CLASSES.SELECTED)) {
                    return true; // jQeury equivalent of continue
                }

                var className;

                if(goodSelectionIndexes[index] != -1){
                    className = presenter.CSS_CLASSES.CORRECT_SELECTED;
                }else{
                    className = presenter.CSS_CLASSES.WRONG_SELECTED;
                }

                $(this).addClass(className);
            });
        }
    }

    function cleanAnswersStyles() {
        // Clearing correct/uncorrect styles which where set when user
        // pressed 'check answers' button.
        gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}`).each(function() {
            if($(this).hasClass(presenter.CSS_CLASSES.CORRECT_SELECTED)) {
                $(this).removeClass(presenter.CSS_CLASSES.CORRECT_SELECTED);
            }
            if($(this).hasClass(presenter.CSS_CLASSES.WRONG_SELECTED)) {
                $(this).removeClass(presenter.CSS_CLASSES.WRONG_SELECTED);
            }
        });
    }

    presenter.reset = function() {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }

        presenter.isSelectionPossible = true;

        cleanAnswersStyles();

        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                gridSelection[row][column] = false;
                applySelectionStyle(row, column);
            }
        }
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.configuration.isVisible = presenter.isVisibleByDefault;
    };

    presenter.setShowErrorsMode = function() {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }

        presenter.isSelectionPossible = false;
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        goodSelectionIndexes = presenter.convertSelectionToIndexes(goodSelections, rows, columns);

        applyAnswerStyles();
    };

    presenter.setWorkMode = function() {
        cleanAnswersStyles();

        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                applySelectionStyle(row, column);
            }
        }

        presenter.isSelectionPossible = true;
    };

    presenter.getMaxScore = function() {
        if(presenter.configuration.isError){
            return 0;
        }

        return maxScore;
    };

    presenter.getScore = function() {
        if(presenter.configuration.isError){
            return 0;
        }

        if(presenter.configuration.checkByWords){
            return presenter.countScoreForWords().score;
        }else{
            return presenter.calculateScore(goodSelections, gridSelection).correct;
        }
    };

    presenter.getErrorCount = function() {
        if(presenter.configuration.isError){
            return 0;
        }

        if(presenter.configuration.checkByWords){
            if(presenter.isAttempted()){
                return presenter.countScoreForWords().errors;
            }else{
                return 0;
            }
        }else{
            return presenter.calculateScore(goodSelections, gridSelection).errors;
        }
    };

    presenter.serializeGridSelection = function(gridSelection, rows, columns) {
        var serializedArray = '';
        var counter = 0;

        for(var row = 0; row &lt; rows; row++) {
            for(var column = 0; column &lt; columns; column++) {
                if(gridSelection[row][column]) {
                    counter++;
                    var index = row * columns + column;
                    serializedArray += index.toString() + ',';
                }
            }
        }

        // Remove last comma separator
        if(counter !== 0) {
            serializedArray = serializedArray.substr(0, serializedArray.length - 1);
        }

        return serializedArray;
    };

    presenter.deserialiseGridSelection = function(serializedArray) {
        var deserialisedArray = [];
        if (serializedArray.length === 0) {
            return deserialisedArray;
        }

        var splittedArray = serializedArray.split(',');

        for(var i = 0; i &lt; splittedArray.length; i++) {
            var index = parseInt(splittedArray[i], 10);
            deserialisedArray.push(index);
        }

        return deserialisedArray;
    };

    presenter.getState = function() {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        return JSON.stringify({
            serializeGridSelection: presenter.serializeGridSelection(gridSelection, rows, columns),
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function(state) {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;
        var row;
        var column;

        initGridSelection();

        var serializeGridSelection, parsedState;
        if (state.indexOf("}") &gt; -1 &amp;&amp; state.indexOf("{") &gt; -1){
            parsedState = JSON.parse(state);
            serializeGridSelection = parsedState.serializeGridSelection;
        }else{
            serializeGridSelection = state;
            parsedState = undefined;
        }

        var dematerialisedState = presenter.deserialiseGridSelection(serializeGridSelection);

        for(var i = 0; i &lt; dematerialisedState.length; i++) {
            row = parseInt(dematerialisedState[i] / columns, 10);
            column = parseInt(dematerialisedState[i] % columns, 10);
            gridSelection[row][column] = true;
        }

        for(row = 0; row &lt; rows; row++) {
            for(column = 0; column &lt; columns; column++) {
                applySelectionStyle(row, column);
            }
        }

        if(parsedState){
            if(parsedState.isVisible != undefined){
                presenter.setVisibility(parsedState.isVisible);
                presenter.configuration.isVisible = parsedState.isVisible;
            }
        }
    };

    presenter.initializeCorrectAnswerLocations = function () {
        for(var i = 0; i&lt; presenter.configuration.answers.length; i++){
            presenter.answerWords[presenter.configuration.answers[i].toString().toLowerCase()] = {
                row : [],
                column : []
            };
            presenter.GSAcorrectAnswerLocations[presenter.configuration.answers[i].toString().toLowerCase()] = {
                row : [],
                column : []
            };
        }
    };

    function presenterLogic(view, model, isPreview){
        presenter.view = view;
        presenter.$view = $(view);
        gridContainerWrapper = presenter.$view.find(`.${presenter.CSS_CLASSES.GRID_WRAPPER}:first`);
        gridContainer = gridContainerWrapper.find(".magicGrid:first");

        let upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        presenter.setSpeechTexts(upgradedModel['speechTexts']);

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        if(presenter.configuration.answers){
            presenter.initializeCorrectAnswerLocations();
        }

        if(presenter.configuration.isError) {
            showErrorMessage(presenter.configuration.errorMessage);
        } else {
            initGridSelection();
            initGrid(model, isPreview);
            goodSelections = presenter.findGoodSelections(presenter.configuration.gridElements, presenter.configuration.answers);

            if(presenter.configuration.checkByWords){
                maxScore = presenter.configuration.answers.length;
            }else{
                maxScore = presenter.calculateMaxScore(goodSelections);
            }

            if (isPreview) {
                gridSelection = goodSelections;
                presenter.setWorkMode();
            } else {
                presenter.buildKeyboardController();
            }
        }
    }

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeLangTag(model);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.upgradeLangTag = function (model) {
        return presenter.upgradeAttribute(model, "langAttribute", '');
    };

    presenter.upgradeSpeechTexts = function (model) {
        const upgradedModel = presenter.upgradeAttribute(model, "speechTexts", {});

        const modelSpeechTexts = upgradedModel["speechTexts"];
        const properties = ["Cell", "Selected", "Deselected", "Correct", "Wrong"];
        properties.forEach(name =&gt; {
            if (!modelSpeechTexts[name]) modelSpeechTexts[name] = {[name]: ""}
        })

        return upgradedModel;
    };

    presenter.upgradeAttribute = function (model, attrName, defaultValue) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (model[attrName] === undefined) {
            upgradedModel[attrName] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            Cell: presenter.DEFAULT_TTS_PHRASES.CELL,
            Selected: presenter.DEFAULT_TTS_PHRASES.SELECTED,
            Deselected: presenter.DEFAULT_TTS_PHRASES.DESELECTED,
            Correct: presenter.DEFAULT_TTS_PHRASES.CORRECT,
            Wrong: presenter.DEFAULT_TTS_PHRASES.WRONG,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Cell: TTSUtils.getSpeechTextProperty(
                speechTexts.Cell.Cell,
                presenter.speechTexts.Cell),
            Selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.Selected),
            Deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.Deselected),
            Correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.Correct),
            Wrong: TTSUtils.getSpeechTextProperty(
                speechTexts.Wrong.Wrong,
                presenter.speechTexts.Wrong)
        };
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model) {
        presenter.addonID = model.ID;
        presenterLogic(view, model, false);
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
        eventBus.addEventListener('GradualShowAnswers', this);
        eventBus.addEventListener('GradualHideAnswers', this);
    };

    presenter.isWordInRow = function(grid, row, word) {
        word = word.toLowerCase();
        var positions = [];

        if (word.length &gt; grid[row].length) {
            return {
                wordFound: false,
                positions: positions
            };
        }

        for (var i = 0; i &lt; (grid[row].length - word.length + 1); i++) {
            var gridWord = "";

            for (var j = 0; j &lt; word.length; j++) {
                gridWord += grid[row][j + i].toLowerCase();
            }

            if (gridWord === word || reverseString(gridWord) === word) {
                positions.push(i);
            }
        }

        return {
            wordFound: positions.length &gt; 0,
            positions: positions
        };
    };

    presenter.isWordInColumn = function(grid, column, word) {
        var positions = [];
        word = word.toLowerCase();

        if (word.length &gt; grid.length) {
            return {
                wordFound: false,
                positions: positions
            };
        }

        for (var i = 0; i &lt; (grid.length - word.length + 1); i++) {
            var gridWord = "";

            for (var j = 0; j &lt; word.length; j++) {
                gridWord += grid[j + i][column].toLowerCase();
            }

            if (gridWord === word || reverseString(gridWord) === word) {
                positions.push(i);
            }
        }

        return {
            wordFound: positions.length &gt; 0,
            positions: positions
        };
    };

    presenter.DIAGONALS = {
        NORMAL: 0,
        REVERSED: 1
    };

    presenter.isWordOnDiagonals = function(grid, word, direction) {
        var rows = grid.length;
        var columns = grid[0].length;
        var positions = [];
        word = word.toLowerCase();

        if (word.length &gt; rows || word.length &gt; columns) {
            return {
                wordFound: false,
                positions: positions
            };
        }

        for (var row = 0; row &lt; rows; row++) {
            for (var column = 0; column &lt; columns; column++) {
                var columnCondition = direction === presenter.DIAGONALS.NORMAL ? word.length &lt;= columns - column : word.length &lt;= column + 1;
                var isWordSmallEnough = word.length &lt;= (rows - row);
                isWordSmallEnough = isWordSmallEnough &amp;&amp; columnCondition;

                if (isWordSmallEnough) {
                    var gridWord = "";

                    for (var i = 0; i &lt; word.length; i++) {
                        var columnIndex = direction === presenter.DIAGONALS.NORMAL ? column + i : column - i;
                        gridWord += grid[row + i][columnIndex].toLowerCase();
                    }

                    if (gridWord === word || reverseString(gridWord) === word) {
                        positions.push({
                            row: row,
                            column: column
                        });
                    }
                }
            }
        }

        return {
            wordFound: positions.length &gt; 0,
            positions: positions
        };
    };

    presenter.findGoodSelections = function (grid, answers) {
        var goodSelections = [];
        var l;

        for (var i = 0; i &lt; answers.length; i++) {
            var answer = answers[i].toString().toLowerCase();

            goodSelections = presenter.findGoodHorizontalSelections(grid, answer, goodSelections);
            goodSelections = presenter.findGoodVerticalSelections(grid, answer, goodSelections);
            goodSelections = presenter.findGoodDiagonalSelections(grid, answer, goodSelections);
            goodSelections = presenter.findGoodReverseDiagonalSelections(grid, answer, goodSelections);
        }

        var rows = grid.length;
        var columns = grid[0].length;

        return presenter.convertSelectionToArray(goodSelections, rows, columns);
    };

    presenter.findGoodHorizontalSelections = function (grid, answer, goodSelections) {
        for (var r = 0; r &lt; grid.length; r++) {
            var horizontalResult = presenter.isWordInRow(grid, r, answer);
            for (var hr = 0; hr &lt; horizontalResult.positions.length; hr++) {
                for (l = 0; l &lt; answer.length; l++) {
                    if(presenter.configuration.checkByWords){
                        presenter.answerWords[answer].row.push(r);
                        presenter.answerWords[answer].column.push(horizontalResult.positions[hr] + l);
                    }
                    
                    presenter.GSAcorrectAnswerLocations[answer].row.push(r);
                    presenter.GSAcorrectAnswerLocations[answer].column.push(horizontalResult.positions[hr] + l);

                    goodSelections.push({
                        row: r,
                        column: horizontalResult.positions[hr] + l
                    });
                }
            }
        }
        return goodSelections;
    }

    presenter.findGoodVerticalSelections = function (grid, answer, goodSelections) {
        for (var c = 0; c &lt; grid[0].length; c++) {
            var verticalResult = presenter.isWordInColumn(grid, c, answer);
            for (var vr = 0; vr &lt; verticalResult.positions.length; vr++) {
                for (l = 0; l &lt; answer.length; l++) {
                    if(presenter.configuration.checkByWords){
                        presenter.answerWords[answer].row.push(verticalResult.positions[vr] + l);
                        presenter.answerWords[answer].column.push(c);
                    }

                    presenter.GSAcorrectAnswerLocations[answer].row.push(verticalResult.positions[vr] + l);
                    presenter.GSAcorrectAnswerLocations[answer].column.push(c);

                    goodSelections.push({
                        row: verticalResult.positions[vr] + l,
                        column: c
                    });
                }
            }
        }
        return goodSelections;
    }

    presenter.findGoodDiagonalSelections = function (grid, answer, goodSelections) {
        var diagonalResult = presenter.isWordOnDiagonals(grid, answer, presenter.DIAGONALS.NORMAL);
        for (var dr = 0; dr &lt; diagonalResult.positions.length; dr++) {
            for (l = 0; l &lt; answer.length; l++) {
                if(presenter.configuration.checkByWords){
                    presenter.answerWords[answer].row.push(diagonalResult.positions[dr].row + l);
                    presenter.answerWords[answer].column.push(diagonalResult.positions[dr].column + l);
                }

                presenter.GSAcorrectAnswerLocations[answer].row.push(diagonalResult.positions[dr].row + l);
                presenter.GSAcorrectAnswerLocations[answer].column.push(diagonalResult.positions[dr].column + l);

                goodSelections.push({
                    row: diagonalResult.positions[dr].row + l,
                    column: diagonalResult.positions[dr].column + l
                });
            }
        }
        return goodSelections;
    }

    presenter.findGoodReverseDiagonalSelections = function (grid, answer, goodSelections) {
        var reverseDiagonalResult = presenter.isWordOnDiagonals(grid, answer, presenter.DIAGONALS.REVERSED);
        for (var rdr = 0; rdr &lt; reverseDiagonalResult.positions.length; rdr++) {
            for (l = 0; l &lt; answer.length; l++) {
                if(presenter.configuration.checkByWords){
                    presenter.answerWords[answer].row.push(reverseDiagonalResult.positions[rdr].row + l);
                    presenter.answerWords[answer].column.push(reverseDiagonalResult.positions[rdr].column - l);
                }
                    
                presenter.GSAcorrectAnswerLocations[answer].row.push(reverseDiagonalResult.positions[rdr].row + l);
                presenter.GSAcorrectAnswerLocations[answer].column.push(reverseDiagonalResult.positions[rdr].column - l);

                goodSelections.push({
                    row: reverseDiagonalResult.positions[rdr].row + l,
                    column: reverseDiagonalResult.positions[rdr].column - l
                });
            }
        }
        return goodSelections;
    }

    presenter.countScoreForWords = function () {
        var score = 0;
        var errors = 0;

        clearCorrectAnswers();
        for (var i=0; i&lt;presenter.configuration.answers.length; i++){
            if(presenter.checkIfWordIsSelected(presenter.configuration.answers[i].toString())){
                fillCorrectAnswers(presenter.configuration.answers[i].toString());
                score++;
            }
        }

        for (var j=0; j&lt;presenter.configuration.rows; j++){
            for (var k=0; k&lt;presenter.configuration.columns; k++){
                if(gridSelection[j][k]  &amp;&amp; !correctAnswers[j][k]){
                    errors++;
                }
            }
        }

        return {
            score: score,
            errors: errors
        };
    };

    function fillCorrectAnswers(word) {
        for (var i = 0; i&lt;presenter.answerWords[word.toLowerCase()].column.length; i++ ){
            correctAnswers[presenter.answerWords[word.toLowerCase()].row[i]][presenter.answerWords[word.toLowerCase()].column[i]] = true;
        }
    }

    presenter.checkIfWordIsSelected = function(word) {
        var correct = 0;
        var incorrect= 0;

        for(var i=0; i &lt; presenter.answerWords[word.toLowerCase()].column.length; i++){
            if(gridSelection[presenter.answerWords[word.toLowerCase()].row[i]][presenter.answerWords[word.toLowerCase()].column[i]]){
                correct++;
            }else{
                incorrect++;
            }
        }
        return correct&gt;0 &amp;&amp; incorrect == 0;
    };

    presenter.convertSelectionToArray = function (selections, rows, columns) {
        var selectionArray = [];
        for (var i = 0; i &lt; rows; i++) {
            selectionArray[i] = [];

            for (var j = 0; j &lt; columns; j++) {
                selectionArray[i][j] = false;
            }
        }

        for (var s = 0; s &lt; selections.length; s++) {
            var row = selections[s].row;
            var column = selections[s].column;

            selectionArray[row][column] = true;
        }

        return selectionArray;
    };

    presenter.convertSelectionToIndexes = function (selections, rows, columns) {
        var selectionIndexes = [];

        for(var row = 0; row &lt; rows; row++) {
            for(var column = 0; column &lt; columns; column++) {
                var index = row * columns + column;
                if (selections[row][column]) {
                    selectionIndexes[index] = 1;
                } else {
                    selectionIndexes[index] = -1;
                }
            }
        }

        return selectionIndexes;
    };

    presenter.calculateMaxScore = function(goodSelections) {
        var rows = goodSelections.length;
        var columns = goodSelections[0].length;

        var maxScore = 0;

        for (var row = 0; row &lt; rows; row++) {
            for (var column = 0; column &lt; columns; column++) {
                if (goodSelections[row][column]) {
                    maxScore++;
                }
            }
        }

        return maxScore;
    };

    presenter.calculateScore = function (goodSelections, selections) {
        var correct = 0;
        var errors = 0;

        var rows = goodSelections.length;
        var columns = goodSelections[0].length;

        for (var row = 0; row &lt; rows; row++) {
            for (var column = 0; column &lt; columns; column++) {
                if (selections[row][column]) {
                    if (selections[row][column] === goodSelections[row][column]) {
                        correct++;
                    } else {
                        errors++;
                    }
                }
            }
        }

        return {
            correct: correct,
            errors: errors
        };
    };

    presenter.validateGrid = function(gridDefinition) {
        var gridElements = [];
        var columnCount = 0;
        var rowCount = 0;

        if(typeof gridDefinition === "undefined" || gridDefinition.length === 0) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.GRID_NOT_PROVIDED
            };
        }

        // Following StackOverflow suggestion : replace -&gt; split
        // http://stackoverflow.com/questions/5034781/js-regex-to-split-by-line
        var rowsArray = $(gridDefinition.split("\n"));
        for (var i = 0; i &lt; rowsArray.length; i++) {
            var row = String(rowsArray[i]);
            if (row.length === 0) {
                return {
                    isError: true,
                    errorMessage: presenter.ERROR_MESSAGES.ROWS
                };
            }

            if(row.search(/[\s\,\;]+/gm) != -1) { // Search for whitespace characters
                return {
                    isError: true,
                    errorMessage: presenter.ERROR_MESSAGES.ROWS
                };
            }

            gridElements[i] = row.split("");

            // Parsing grid definition
            if(rowCount === 0) {
                columnCount = row.length;
            }

            if(columnCount != row.length) {
                return {
                    isError: true,
                    errorMessage: presenter.ERROR_MESSAGES.COLUMNS
                };
            }

            rowCount++;
        }

        return {
            isError: false,
            columns: columnCount,
            rows: rowCount,
            gridElements: gridElements
        };
    };

    presenter.validateAnswers = function(answersDefinition) {
        var answers = [];

        if(typeof answersDefinition === "undefined" || answersDefinition.length === 0) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.ANSWERS_NOT_PROVIDED
            };
        }

        $(answersDefinition.split("\n")).each(function() {
            var answer = String(this);
            $(answer.split(/[\s\,\;]+/gm)).each(function() {
                answers.push(this);
            });
        });

        return {
            isError: false,
            answers: answers
        };
    };

    presenter.validateModel = function(model) {
        var gridDefinition = model.Grid;
        var answersDefinition = model.Answers;

        var gridValidationResult = presenter.validateGrid(gridDefinition);
        if (gridValidationResult.isError) {
            return {
                isError: true,
                errorMessage: gridValidationResult.errorMessage
            };
        }

        var answersValidationResult = presenter.validateAnswers(answersDefinition);
        if (answersValidationResult.isError) {
            return {
                isError: true,
                errorMessage: answersValidationResult.errorMessage
            };
        }

        return {
            isError: false,
            columns: gridValidationResult.columns,
            rows: gridValidationResult.rows,
            gridElements: gridValidationResult.gridElements,
            answers: answersValidationResult.answers,
            checkByWords: ModelValidationUtils.validateBoolean(model['CheckByWords']),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            langTag: model['langAttribute']
        };
    };

    presenter.executeCommand = function (name, params) {
        if (!presenter.isSelectionPossible) return;

        var commands = {
            'isAllOK': presenter.isAllOK,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.createAllOKEventData = function () {
        return {
            'source': presenter.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };
    };

    presenter.createEventData = function (item, value, score) {
        return {
            'source': presenter.addonID,
            'item': item,
            'value': value,
            'score': score
        };
    };

    presenter.onEventReceived = function (eventName, data) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "HideAnswers":
                presenter.hideAnswers();
                break;

            case "GradualShowAnswers":
                presenter.gradualShowAnswers(data);
                break;

            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    function applyShowAnswerStyles() {
        gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}`).each(function(index) {

            var className;

            if(goodSelectionIndexes[index] != -1){
                className = presenter.CSS_CLASSES.SHOW_ANSWERS;
            }

            $(this).addClass(className);
        });
    }

    function cleanShowAnswersStyles() {
        gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}`).each(function() {
            if($(this).hasClass(presenter.CSS_CLASSES.SHOW_ANSWERS)) {
                $(this).removeClass(presenter.CSS_CLASSES.SHOW_ANSWERS);
            }
        });
    }

    presenter.isAttempted = function () {
        for (var i = 0; i&lt;presenter.configuration.rows; i++){
            for (var j = 0; j&lt;presenter.configuration.columns; j++){
                if(gridSelection[i][j] == true){
                    return true;
                }
            }
        }
        return false;
    };

    function checkIfSelected (row, column){
        var index = row * presenter.configuration.columns + column;
        var element = gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:eq(${index})`);

        if(gridSelection[row][column]) {// is selected
            if(element.hasClass(presenter.CSS_CLASSES.SELECTED)) {
                element.removeClass(presenter.CSS_CLASSES.SELECTED);
                return true;
            }else{
                return false;
            }
        }
        return false;
    }

    function addClassToSelectedElement (row, column){
        var index = row * presenter.configuration.columns + column;
        var element = gridContainerWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:eq(${index})`);
            element.addClass(presenter.CSS_CLASSES.SELECTED);
    }

    presenter.getActivitiesCount = function() {
        if(presenter.configuration.checkByWords){
            return presenter.configuration.answers.length;
        } else {
            goodSelectionIndexes = presenter.convertSelectionToIndexes(goodSelections, presenter.configuration.rows, presenter.configuration.columns);
            let numberOfLetters = 0;
            for (let i = 0; i &lt; goodSelectionIndexes.length; i++) {
                if (goodSelectionIndexes[i] == 1) numberOfLetters++;
            }
            return numberOfLetters;
        }
    }

    presenter.saveUserSelectionBeforeShowAnswers = function () {
        presenter.isSelected = [];
        presenter.setWorkMode();

        for(var row1 = 0; row1 &lt; presenter.configuration.rows; row1++) {
            presenter.isSelected[row1] = [];
            for(var column1 = 0; column1 &lt; presenter.configuration.columns; column1++) {
                presenter.isSelected[row1][column1] = 0;
            }
        }

        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                if(checkIfSelected(row, column)){
                    presenter.isSelected[row][column] = 1;
                }
            }
        }
    }

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
        presenter.saveUserSelectionBeforeShowAnswers();

        presenter.isSelectionPossible = false;
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        goodSelectionIndexes = presenter.convertSelectionToIndexes(goodSelections, rows, columns);

        applyShowAnswerStyles();
    };

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        cleanShowAnswersStyles();

        for(var row = 0; row &lt; presenter.configuration.rows; row++) {
            for(var column = 0; column &lt; presenter.configuration.columns; column++) {
                if(presenter.isSelected[row][column] == 1){
                    addClassToSelectedElement(row, column);
                }
            }
        }

        presenter.isSelectionPossible = true;

        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
    };

    presenter.gradualShowAnswers = function (data) {
        if(data.moduleID !== presenter.addonID) return;

        if(!presenter.isGradualShowAnswersActive){
            presenter.saveUserSelectionBeforeShowAnswers();
        }

        presenter.isShowAnswersActive = true;
        presenter.isGradualShowAnswersActive = true;
        presenter.isSelectionPossible = false;

        for(const key in presenter.GSAcorrectAnswerLocations) {
            if (usedGSAKeys.includes(key)) {
                continue;
            } else if (presenter.configuration.checkByWords) {
                usedGSAKeys.push(key);
            }
            if (!presenter.GSAcorrectAnswerLocations.hasOwnProperty(key)) continue;

            const answer = presenter.GSAcorrectAnswerLocations[key];
            const row = answer["row"];
            const column = answer["column"];
            if (row.length !== column.length) continue;
            
            presenter.configuration.checkByWords
                    ? presenter.showNextWord(row, column)
                    : presenter.showNextLetter(key, row, column);

            break;
        }
    };

    presenter.showNextWord = function (row, column) {
        const columns = presenter.configuration.columns;

        for (let i = 0; i &lt; row.length; i++){
            let index = row[i] * columns + column[i];
            let element = gridContainerWrapper.find(`.selectable-element:eq(${index})`);
            element.addClass('selectable-element-show-answers');
        }
    };

    presenter.showNextLetter = function (key, row, column) { 
        const columns = presenter.configuration.columns;

        var element;
        do {
            let index = row[GSALetterCounter] * columns + column[GSALetterCounter];
            element = gridContainerWrapper.find(`.selectable-element:eq(${index})`);
            GSALetterCounter++;
        } while(element.hasClass("selectable-element-show-answers"));
        element.addClass('selectable-element-show-answers');

        if (GSALetterCounter === row.length) {
            usedGSAKeys.push(key);
            GSALetterCounter = 0;
        }
    };

    presenter.gradualHideAnswers = function () {
        if (!presenter.isGradualShowAnswersActive) {
            return;
        }

        GSALetterCounter = 0;
        usedGSAKeys = [];

        presenter.hideAnswers();
    };

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    function MagicBoxesKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
        this.updateMapping();
    }

    MagicBoxesKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    MagicBoxesKeyboardController.prototype.constructor = MagicBoxesKeyboardController;

    presenter.buildKeyboardController = function () {
        const keyNavElements = presenter.getElementsForKeyboardNavigation();
        presenter.keyboardControllerObject = new MagicBoxesKeyboardController(
            keyNavElements, presenter.configuration.columns
        );
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find(`.${presenter.CSS_CLASSES.ELEMENT_WRAPPER}`);
    };

    MagicBoxesKeyboardController.prototype.updateMapping = function () {
        this.mapping[window.KeyboardControllerKeys.TAB] = this.nextTabElement;
        this.shiftKeysMapping[window.KeyboardControllerKeys.TAB] = this.previousTabElement;
    };

    MagicBoxesKeyboardController.prototype.nextTabElement = function (event) {
        KeyboardController.prototype.nextElement.call(this, event);
    };

    MagicBoxesKeyboardController.prototype.previousTabElement = function (event) {
        KeyboardController.prototype.previousElement.call(this, event);
    };

    MagicBoxesKeyboardController.prototype.nextElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        const newPositionIndex = this.calculateNewPositionIndexForHorizontalMove(1);
        if (newPositionIndex !== undefined) {
            this.markCurrentElement(newPositionIndex);
        }
    };

    MagicBoxesKeyboardController.prototype.previousElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        const newPositionIndex = this.calculateNewPositionIndexForHorizontalMove(-1);
        if (newPositionIndex !== undefined) {
            this.markCurrentElement(newPositionIndex);
        }
    };

    MagicBoxesKeyboardController.prototype.nextRow = function (event) {
        if (event) {
            event.preventDefault();
        }
        const newPositionIndex = this.calculateNewPositionIndexForVerticalMove(this.columnsCount);
        if (newPositionIndex !== undefined) {
            this.markCurrentElement(newPositionIndex);
        }
    };

    MagicBoxesKeyboardController.prototype.previousRow = function (event) {
        if (event) {
            event.preventDefault();
        }
        const newPositionIndex = this.calculateNewPositionIndexForVerticalMove(-this.columnsCount);
        if (newPositionIndex !== undefined) {
            this.markCurrentElement(newPositionIndex);
        }
    };

    MagicBoxesKeyboardController.prototype.calculateNewPositionIndexForHorizontalMove = function (move) {
        return this.calculateNewPositionIndexForMoveInOneAxis(move, true);
    };

    MagicBoxesKeyboardController.prototype.calculateNewPositionIndexForVerticalMove = function (move) {
        return this.calculateNewPositionIndexForMoveInOneAxis(move, false);
    };

    MagicBoxesKeyboardController.prototype.calculateNewPositionIndexForMoveInOneAxis = function (move, isHorizontal = true) {
        const oldPosition = this.calculatePosition();
        const newPosition = this.calculatePosition(move);
        const newPositionIndex = this.keyboardNavigationCurrentElementIndex + move;
        if (move === 0
            || (isHorizontal &amp;&amp; newPosition.y !== oldPosition.y)
            || (!isHorizontal &amp;&amp; newPosition.x !== oldPosition.x)
            || (newPositionIndex &gt;= this.keyboardNavigationElementsLen)
            || (newPositionIndex &lt; 0)) {
            return undefined;
        }
        return newPositionIndex;
    };

    MagicBoxesKeyboardController.prototype.calculatePosition = function (move = 0) {
        const newIndex = this.keyboardNavigationCurrentElementIndex + move;
        return {
            x: newIndex % this.columnsCount,
            y: Math.floor(newIndex / this.columnsCount)
        }
    };

    MagicBoxesKeyboardController.prototype.markCurrentElement = function (new_position_index) {
        KeyboardController.prototype.markCurrentElement.call(this, new_position_index);

        if (presenter.isTTS()) {
            this.readCurrentElement();
        }
    };

    MagicBoxesKeyboardController.prototype.enter = function (event) {
        const isFirstEnter = !this.keyboardNavigationActive;
        KeyboardController.prototype.enter.call(this, event);

        if (presenter.isTTS() &amp;&amp; !isFirstEnter) {
            this.readCurrentElement();
        }
    };

    MagicBoxesKeyboardController.prototype.select = function (event) {
        if (event) {
            event.preventDefault();
        }

        if (!this.isSelectEnabled || !presenter.isSelectionPossible) {
            return;
        }

        const $currentSelectableElement = this.getCurrentSelectableElement();
        const hadSelectedClass = $currentSelectableElement.hasClass(presenter.CSS_CLASSES.SELECTED);

        this.selectAction();

        const haveSelectedClass = $currentSelectableElement.hasClass(presenter.CSS_CLASSES.SELECTED);
        const classChanged = hadSelectedClass !== haveSelectedClass;
        if (classChanged) {
            if (haveSelectedClass) {
                readSelected();
            } else {
                readDeselected();
            }
        }
    };

    MagicBoxesKeyboardController.prototype.selectAction = function () {
        const currentPosition = this.calculatePosition();
        selectionHandler(currentPosition.y, currentPosition.x);
    };

    MagicBoxesKeyboardController.prototype.getCurrentSelectableElement = function () {
        const $elementWrapper = this.getCurrentElement();
        return $elementWrapper.find(`.${presenter.CSS_CLASSES.ELEMENT}:first`);
    };

    MagicBoxesKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    MagicBoxesKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    MagicBoxesKeyboardController.prototype.readCurrentElement = function () {
        const $element = this.getCurrentSelectableElement();

        let textVoices = window.TTSUtils.getTextVoiceArrayFromElement($element, presenter.configuration.langTag);
        this.unshiftElementIndexTextVoice(textVoices);
        this.addElementStateInfoTextVoices(textVoices, $element);

        presenter.speak(textVoices);
    };

    MagicBoxesKeyboardController.prototype.unshiftElementIndexTextVoice = function (textVoices) {
        const alphabet = "ABCDEFGHIJKLMNOPRSTUWXYZ";
        const position = this.calculatePosition();
        const message = presenter.speechTexts.Cell
            + " " + alphabet[position.x % alphabet.length]
            + " " + (position.y + 1);
        textVoices.unshift(window.TTSUtils.getTextVoiceObject(message));
    };

    MagicBoxesKeyboardController.prototype.addElementStateInfoTextVoices = function (textVoices, $selectableElement) {
        if ($selectableElement.hasClass(presenter.CSS_CLASSES.SELECTED)
            || $selectableElement.hasClass(presenter.CSS_CLASSES.SHOW_ANSWERS)) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.Selected);
        }

        switch (true) {
            case ($selectableElement.hasClass(presenter.CSS_CLASSES.CORRECT_SELECTED)):
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.Correct);
                break;
            case ($selectableElement.hasClass(presenter.CSS_CLASSES.WRONG_SELECTED)):
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.Wrong);
                break;
            default:
                break;
        }
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    MagicBoxesKeyboardController.prototype.exitWCAGMode = function () {
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    function readSelected () {
        let textVoices = [];
        addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.Selected);
        presenter.speak(textVoices);
    }

    function readDeselected () {
        let textVoices = [];
        addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.Deselected);
        presenter.speak(textVoices);
    }

    function addTextVoiceWithLanguageFromLesson(textVoices, message) {
        textVoices.push(window.TTSUtils.getTextVoiceObject(message));
    }

    presenter.isTTS = function () {
        return presenter.isWCAGOn &amp;&amp; presenter.getTextToSpeechOrNull(playerController);
    };

    presenter.speak = function(data) {
        const tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Math" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Variables" nameLabel="Math_property_variables" type="text"/>
        <property name="Expressions" nameLabel="Math_property_expressions" type="text"/>
        <property name="Show Answers" nameLabel="Math_property_show_answers" type="text"/>
        <property name="Decimal separator" nameLabel="Math_property_decimal_separator" type="string"/>
        <property name="Thousand separator" nameLabel="Math_property_thousand_separator" type="string"/>
        <property name="onCorrect" nameLabel="Math_property_on_correct" type="event"/>
        <property name="onIncorrect" nameLabel="Math_property_on_incorrect" type="event"/>
        <property name="onPartiallyCompleted" nameLabel="Math_property_on_partially_completed" type="event"/>
        <property name="Empty Answer" nameLabel="Math_property_empty_answer" type="string"/>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
    </model>
<css/><view/><preview/><presenter>function AddonMath_create() {

    function getCorrectObject(val) { return { isError: false, value: val }; }
    function getErrorObject(ec) { return { isError: true, errorCode: ec }; }

    var presenter = function() {};
    presenter.isShowAnswers = false;
    presenter.currentGapIndex = 0;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.setEventBus = function (eventBus) {
        presenter.eventBus = eventBus;
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeAddPrintable(model);
    }

    presenter.upgradeAddPrintable = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['printable'] === undefined) {
            upgradedModel['printable'] = "No";
        }

        return upgradedModel;
    }

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model);
        presenter.$view.css('visibility', 'hidden');

        if (presenter.eventBus) {
            presenter.eventBus.addEventListener('ShowAnswers', this);
            presenter.eventBus.addEventListener('HideAnswers', this);
            presenter.eventBus.addEventListener('PageLoaded', this);
            presenter.eventBus.addEventListener('ValueChanged', this);
            presenter.eventBus.addEventListener('GradualShowAnswers', this);
            presenter.eventBus.addEventListener('GradualHideAnswers', this);
        }
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model);
    };

    presenter.presenterLogic = function (view, model) {
        let upgradedModel = presenter.upgradeModel(model);
        presenter.$view = $(view);
        presenter.model = upgradedModel;
        presenter.configuration = presenter.convertModel(upgradedModel);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
        }
    };

    presenter.ERROR_CODES = {
        'CV_01': "Missing assignment operator!",
        'CV_02': "Missing gap ID!",
        'CV_03': "Unused variable!",
        'CV_04': "Decimal separator and thousand separator are the same!",
        'CV_05': "Number of defined gaps in Show Answers is different than number of Variables",
        'CV_06': "Incorrect gap name defined in Show Answers property",
        'SA04': "Empty line is inserted in Show Answers property",
        'SA02': "Name of gap is not defined in Show Answers",
        'SA03': "Value of gap is not defined in Show Answers",
        'EV_01': "Only one string can be declared"
    };

    presenter.convertVariables = function (variables, expressions) {
        if (ModelValidationUtils.isStringEmpty(variables)) return { isError: false, variables: [] };

        var variablesArray = [], splittedVariables = variables.split('\n'), i, j, expVariables = [];

        for (i = 0; i &lt; splittedVariables.length; i++) {
            var line = splittedVariables[i];
            if (line.indexOf('=') === -1) return { isError: true, errorCode: 'CV_01' };

            var splittedLine = line.split('=');
            if (splittedLine.length !== 2 || splittedLine[1].length === 0) return { isError: true, errorCode: 'CV_02' };

            variablesArray.push({
                name: splittedLine[0].trim(),
                value: splittedLine[1].trim()
            });
        }

        for (i = 0; i &lt; expressions.length; i++) {
            var variablesInExp = presenter.selectVariablesFromExpression(expressions[i], variablesArray);
            for (j = 0; j &lt; variablesInExp.length; j++) {
                expVariables.push(variablesInExp[j]);
            }
        }

        for (i = 0; i &lt; variablesArray.length; i++) {
            if (expVariables.indexOf(variablesArray[i].name) === -1) return { isError: true, errorCode: 'CV_03' };
        }

        return { isError: false, variables: variablesArray };
    };

    presenter.parseShowAnswers = function (answers, convertedVariables) {
        if (ModelValidationUtils.isStringEmpty(answers)) return getCorrectObject([]);

        var variables = answers.split('\n').map(function(line) {
            return {
                name:line.substr(0, line.indexOf('=')).trim(),
                value:line.substr(line.indexOf('=') + 1).trim(),
                users: ''
            }
        });

        if (variables.some(function(v) { return v.value === '' &amp;&amp; v.name === ''; })) {
            return getErrorObject('SA04'); // check if empty line is in property
        }

        if (variables.some(function(v) { return v.name === ''; })) {
            return getErrorObject('SA02'); // check if name of gap is defined
        }

        if (variables.some(function(v) { return v.value === ''; })) {
            return getErrorObject('SA03'); // check if value of gap is defined
        }

        if(variables.length &gt; 0 &amp;&amp; (variables.length != convertedVariables.length)){
            return getErrorObject('CV_05'); // check if number of gaps equals number of defined gaps in Show Answers
        }

        var definedGaps = [];
        for (var j = 0; j &lt;  convertedVariables.length; j++){
            definedGaps.push(convertedVariables[j].name);
        }

        for (var i = 0; i &lt;  variables.length; i++){
            if(!(definedGaps.indexOf(variables[i].name) &gt; -1)){
                return getErrorObject('CV_06'); // check if defined gap names are correct
            }
        }

        return getCorrectObject(variables);
    };

    presenter.convertExpressions = function (expressions) {
        var expressionsArray = [], splittedExpressions = expressions.split('\n');

        for (var i = 0; i &lt; splittedExpressions.length; i++) {
            expressionsArray.push(splittedExpressions[i]);
        }

        return expressionsArray;
    };


    presenter.convertEmptyAnswer = function(variable) {
        if (!variable) return "";
        if (variable.indexOf(' ') &gt;= 0) return { isError: true, errorCode: 'EV_01' };

        return variable;
    };

    presenter.convertModel = function (model) {
        var expressions = presenter.convertExpressions(model.Expressions);

        var convertedVariables = presenter.convertVariables(model.Variables, expressions);
        if (convertedVariables.isError) {
            return { isError: true, errorCode: convertedVariables.errorCode };
        }

        var parsedShowAnswers = presenter.parseShowAnswers(model['Show Answers'], convertedVariables.variables);
        if (parsedShowAnswers.isError) {
            return { isError: true, errorCode: parsedShowAnswers.errorCode };
        }

        var decimalSeparator = model["Decimal separator"],
            isDecimalSeparatorSet = !ModelValidationUtils.isStringEmpty(decimalSeparator);

        var thousandSeparator = model["Thousand separator"],
            isThousandSeparatorSet = !ModelValidationUtils.isStringEmpty(thousandSeparator);

        if (decimalSeparator == thousandSeparator &amp;&amp; isDecimalSeparatorSet &amp;&amp; isThousandSeparatorSet) {
            return { isError: true, errorCode: 'CV_04' };
        }

        var separators =  {
            decimalSeparator: isDecimalSeparatorSet ? decimalSeparator : undefined,
            isDecimalSeparatorSet: isDecimalSeparatorSet,
            thousandSeparator: isThousandSeparatorSet ? thousandSeparator : undefined,
            isThousandSeparatorSet: isThousandSeparatorSet
        };

        var emptyAnswer = presenter.convertEmptyAnswer(model['Empty Answer']);
        if (emptyAnswer.isError) {
            return { isError: true, errorCode: 'EV_01' };
        }

        return {
            isError: false,
            variables: convertedVariables.variables,
            expressions: expressions,
            answers: parsedShowAnswers.value,
            onCorrectEvent: model.onCorrect,
            onIncorrectEvent: model.onIncorrect,
            onPartialEvent: model.onPartiallyCompleted,
            separators: separators,
            emptyAnswer: emptyAnswer,
            addonID: model.ID
        };
    };

    function isInExpressionString (expressions) {
        for (var i = 0; i &lt; expressions.length; i++) {
            if (!isNumber(expressions[i].value)) {
                return true;
            }
        }
    }

    function changeExpressionToString (expressions) {
        for (var i = 0; i &lt; expressions.length; i++) {
            expressions[i].value = expressions[i].value + "";
        }
        return expressions;
    }

    presenter.evaluateExpression = function (expression, variables, separators, isPrint) {
        var i, expressionRunner = {
            /**
             * @param  {string} expression
             * @param  {} variables
             */
            run: function (expression, variables) {
                presenter.assignVariablesToObject(this, variables, expression);
                var parser = math.parser();

                parser.set('variables', this.variables);

                if ((expression.indexOf("{") &gt; -1) &amp;&amp; (expression.indexOf("function") &gt; -1)) {     // There will be probably function declaration
                    delete math.iifeFunction;

                    math.import({
                        iifeFunction: new Function(expression + "; return result;")
                    });

                    expression = "result = iifeFunction.call({variables:variables})";
                } else {
                    expression = expression.replace(/&amp;&amp;/g," and ").replace(/\|\|/g, " or ").replace(/'/g, '"');
                }

                parser.eval(expression);
                return parser.get('result');
            }
        };

        try {
            var convertedVariables = [];
            for (i = 0; i &lt; variables.length; i++) {
                var convertedVariable;
                if (!isPrint) {
                    convertedVariable = presenter.convertVariable(variables[i].value, separators);
                } else {
                    convertedVariable = presenter.getVariableValueFromPrintableState(variables[i].value);
                }
                if (convertedVariable === undefined) return { isValid: false, result: getAlertMessage(variables[i]) };
                convertedVariables.push({
                    name: variables[i].name,
                    value: convertedVariable
                });
            }

            if(isInExpressionString(convertedVariables)) {
                convertedVariables = changeExpressionToString(convertedVariables);
            }

            var convertedExpression = presenter.convertExpression(expression, convertedVariables);
            return {isValid: true, result: expressionRunner.run(convertedExpression, convertedVariables)};
        } catch (exception) {
            return {isValid: true, result: false};
        }
    };

    presenter.selectVariablesFromExpression = function (expression, variables) {
        var presentVariables = [], tempVariables = [], i, j;

        for (i = 0; i &lt; variables.length; i++) {
            if (expression.indexOf(variables[i].name) !== -1) {
                tempVariables.push({
                    name: variables[i].name,
                    index: expression.indexOf(variables[i].name)
                });
            }
        }

        for (i = 0; i &lt; tempVariables.length; i++) {
            if (!tempVariables[i]) continue;

            var variable = tempVariables[i];
            for (j = 0; j &lt; tempVariables.length; j++) {
                if (i === j || tempVariables[j] === undefined) continue;

                if (variable.index === tempVariables[j].index) {
                    if (variable.name.length &lt; tempVariables[j].name.length) {
                        delete tempVariables[i];
                    } else {
                        delete tempVariables[j];
                    }
                }
            }
        }

        for (i = 0; i &lt; tempVariables.length; i++) {
            if (tempVariables[i]) presentVariables.push(tempVariables[i].name);
        }

        return presentVariables;
    };

    presenter.evaluateAllExpressions = function (expressions, variables, separators, isPrint = false) {
        var results = [], i, overall = true, evaluationResult;

        for (i = 0; i &lt; expressions.length; i++) {
            evaluationResult = presenter.evaluateExpression(expressions[i], variables, separators, isPrint);
            if (!evaluationResult.isValid) {
                return { isError: true, errorMessage: evaluationResult.result };
            }

            results.push(evaluationResult.result);
        }

        for (i = 0; i &lt; results.length; i++) {
            if (!results[i]) {
                overall = false;
            }
        }

        return { overall: overall, isError: false };
    };

    presenter.getModule = function (moduleID) {
        return presenter.playerController.getModule(moduleID);
    };

    function isNumber (number) {
        return !isNaN(parseFloat(number)) &amp;&amp; isFinite(number);
    }

    presenter.convertVariable = function (gapIdentifier, separators) {
        var decodedReference = presenter.decodeModuleReference(gapIdentifier);
        if (!decodedReference.isValid) return undefined;

        try {
            var textModule = presenter.getModule(decodedReference.moduleID);
            if (!textModule) return undefined;

            var gapText = textModule.getGapValue(decodedReference.gapIndex);
            if (gapText == "[error]") return undefined;

            if (separators.isThousandSeparatorSet) {
                gapText = StringUtils.replaceAll(gapText, separators.thousandSeparator, '');
            }

            if (separators.isDecimalSeparatorSet) {
                gapText = gapText.replace(separators.decimalSeparator, '.');
            }

            return isNumber(gapText) ? Number(gapText) : gapText;
        } catch (exception) {
            return undefined;
        }
    };

    presenter.isGapAttempted = function (gapIdentifier) {
        var decodedReference = presenter.decodeModuleReference(gapIdentifier);

        if (!decodedReference.isValid) return undefined;

        try {
            var textModule = presenter.getModule(decodedReference.moduleID);
            if (!textModule) return undefined;

            return textModule.isGapAttempted(decodedReference.gapIndex);
        } catch (exception) {
            return undefined;
        }
    };

    presenter.convertExpression = function (expression, variables) {
        var convertedExpression = 'result = ' + expression,
            expressionVariables = presenter.selectVariablesFromExpression(expression, variables), i;

        for (i = 0; i &lt; expressionVariables.length; i++) {
            convertedExpression = presenter.replaceVariableNameWithReference(convertedExpression, expressionVariables[i]);
        }

        return convertedExpression;
    };

    function checkIfCorrectVariable(tempExpression, variable) {
        var lastChar = tempExpression.charAt(tempExpression.indexOf(variable)+variable.length);
        return lastChar == "." || lastChar == "(" || lastChar == ")" || lastChar == "" || lastChar == " " || lastChar == "/" || lastChar == "*" || lastChar == "=" || lastChar == "+" || lastChar == "-" || lastChar == "&gt;" || lastChar == "&lt;" || lastChar == "%" || lastChar == "!";
    }

    presenter.findTextOccurrences = function (expression, variable) {
        var indexes = [], tempExpression = expression, offset = 0;

        while (tempExpression.indexOf(variable) !== -1 &amp;&amp; checkIfCorrectVariable(tempExpression, variable)) {
            var indexOf = tempExpression.indexOf(variable);
            indexes.push(indexOf + offset);
            offset += indexOf + variable.length;

            tempExpression = tempExpression.substring(indexOf + variable.length);
        }

        return indexes;
    };

    presenter.replaceVariableNameWithReference = function (expression, variable) {
        var prefix = 'variables["';
        var indexes = presenter.findTextOccurrences(expression, variable);
        var fixedExpression = expression.substring(0, indexes[0]);

        for (var i = 0; i &lt; indexes.length - 1; i++) {
            fixedExpression += prefix + variable + '"]';
            fixedExpression += expression.substring(indexes[i] + variable.length, indexes[i + 1]);
        }

        fixedExpression += prefix + variable + '"]';
        fixedExpression += expression.substring(indexes[indexes.length - 1] + variable.length);

        return fixedExpression;
    };

    presenter.assignVariablesToObject = function (object, variables, expression) {
        object.variables = {};
        if (expression != null) {
            var parsedExpression = expression.replace(/[\s]/g, '').replace(/'/g, '"').replace(/[&lt;&gt;+\-]/g, '=');
            parsedExpression = parsedExpression.replace(/===?/g, '=').replace(/variables\["(.*?)"]/g, '$1');
        }

        for (var i = 0; i &lt; variables.length; i++) {
            var name = variables[i].name;
            if (parsedExpression != null &amp;&amp; (parsedExpression.indexOf('"='+name) != -1 || parsedExpression.indexOf(name+'="') != -1)) {
                object.variables[variables[i].name] = variables[i].value.toString();
            } else {
                object.variables[variables[i].name] = variables[i].value;
            }
        }
    };

    presenter.decodeModuleReference = function (reference) {
        var dotIndex = reference.lastIndexOf('.');
        if (dotIndex === -1) return { isValid: false };

        var moduleID = reference.substring(0, dotIndex);
        if (ModelValidationUtils.isStringEmpty(moduleID)) return { isValid: false };

        var gapIndex = reference.substring(dotIndex + 1);
        if (ModelValidationUtils.isStringEmpty(gapIndex)) return { isValid: false };

        return { isValid: true, moduleID: moduleID, gapIndex: gapIndex };
    };

    presenter.getModuleReferenceFromVariable = function (variables, variableName) {
        for (var i = 0, length = variables.length; i &lt; length; i++) {
            if (variables[i].name === variableName) return variables[i].value;
        }
    };

    presenter.markGapsCorrectness = function (variables, overall) {
        var i, decodedReference, length, textModule;

        for (i = 0, length = variables.length; i &lt; length; i++) {
            decodedReference = presenter.decodeModuleReference(variables[i].value);
            textModule = presenter.playerController.getModule(decodedReference.moduleID);

            if (overall) {
                textModule.markGapAsCorrect(decodedReference.gapIndex);
            } else {
                textModule.markGapAsWrong(decodedReference.gapIndex);
            }
        }
    };

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswers) {
            presenter.toggleAnswers(false, true);
        }

        presenter.isErrorMode = true;

        var variables = presenter.configuration.variables,
            emptyGaps = presenter.getEmptyGaps(variables);

        if (!emptyGaps.isValid) {
            presenter.hideAnswers();
            alert(emptyGaps.errorMessage);
            return;
        }

        presenter.markGapsEmptiness(emptyGaps.gaps);

        if (emptyGaps.gaps.length !== 0) return;

        var separators = presenter.configuration.separators,
            expressions = presenter.configuration.expressions,
            evaluationResult = presenter.evaluateAllExpressions(expressions, variables, separators);

        presenter.markGapsCorrectness(presenter.configuration.variables, evaluationResult.overall);
    };

    presenter.setWorkMode = function() {
        presenter.isErrorMode = false;
    };

    presenter.executeEventCode = function(eventCode) {
        presenter.playerController.getCommands().executeEventCode(eventCode);
    };

    presenter.markGapsEmptiness = function(gaps) {
        var moduleReference, decodedReference, textModule, i, length;

        for (i = 0, length = gaps.length; i &lt; length; i++) {
            moduleReference = presenter.getModuleReferenceFromVariable(presenter.configuration.variables, gaps[i]);
            decodedReference = presenter.decodeModuleReference(moduleReference);
            textModule = presenter.playerController.getModule(decodedReference.moduleID);
            textModule.markGapAsEmpty(decodedReference.gapIndex);
        }
    };

    presenter.evaluate = function () {
        if (presenter.isErrorMode) return;

        var emptyGaps = presenter.getEmptyGaps(presenter.configuration.variables);
        if (!emptyGaps.isValid) {
            presenter.hideAnswers();
            alert(emptyGaps.errorMessage);
            return;
        }

        if (emptyGaps.gaps.length !== 0) {
            presenter.executeEventCode(presenter.configuration.onPartialEvent);
        } else {
            var separators = presenter.configuration.separators,
                evaluationResult = presenter.evaluateAllExpressions(presenter.configuration.expressions,
                                                                    presenter.configuration.variables, separators),
                eventCode = evaluationResult.overall ? presenter.configuration.onCorrectEvent : presenter.configuration.onIncorrectEvent;
            presenter.executeEventCode(eventCode);
        }
    };

    presenter.isAttempted = function() {
        var notAttemptedGaps = presenter.getNotAttemptedGaps(presenter.configuration.variables);
        return notAttemptedGaps.gaps.length === 0;
    };

    presenter.getScore = function () {
        if(presenter.configuration.isError){
            return;
        }
        const isShowAnswers = presenter.isShowAnswers;
        if (isShowAnswers) presenter.toggleAnswers(false);

        const variables = presenter.configuration.variables,
            emptyGaps = presenter.getEmptyGaps(variables);
        if (!emptyGaps.isValid || emptyGaps.gaps.length !== 0) {
            if (isShowAnswers) presenter.toggleAnswers(true);
            return 0;
        }

        const separators = presenter.configuration.separators,
            expressions = presenter.configuration.expressions,
            evaluationResult = presenter.evaluateAllExpressions(expressions, variables, separators);

        if (isShowAnswers) presenter.toggleAnswers(true);
        if (evaluationResult.isError) return;

        return evaluationResult.overall ? presenter.getMaxScore() : 0;
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isError){
            return;
        }

        var isShowAnswers = presenter.isShowAnswers;
        if (isShowAnswers) {
            presenter.toggleAnswers(false);
        }

        var variables = presenter.configuration.variables,
            emptyGaps = presenter.getEmptyGaps(variables);

        if (!emptyGaps.isValid || emptyGaps.gaps.length !== 0) {
            if (isShowAnswers) presenter.toggleAnswers(true);
            return 0;
        }

        var separators = presenter.configuration.separators,
            expressions = presenter.configuration.expressions,
            evaluationResult = presenter.evaluateAllExpressions(expressions, variables, separators);
        if (isShowAnswers) presenter.toggleAnswers(true);

        if (evaluationResult.isError) return;

        return !evaluationResult.overall ? presenter.getMaxScore() : 0;
    };

    presenter.getMaxScore = function () {
        if(presenter.configuration.isError){
            return;
        }
        return presenter.configuration.variables.length;
    };

    presenter.reset = function () {
        presenter.isErrorMode = false;
        presenter.isShowAnswers = false;
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore();
    };

    presenter.createAllOKEventData = function (){
        return{
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        }
    };

    presenter.sendAllOKEvent = function (){
        presenter.eventBus.sendEvent('ValueChanged', presenter.createAllOKEventData());
    };

    presenter.executeCommand = function(name, params) {
        if (presenter.isErrorMode) return;

        var commands = {
            'evaluate': presenter.evaluate,
            'isAttempted': presenter.isAttempted,
            'showAnswers': presenter.showAnswers,
            'hideAnswers': presenter.hideAnswers,
            'isAllOK': presenter.isAllOK
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    function getAlertMessage(variable) {
        var decodedReference = presenter.decodeModuleReference(variable.value);

        return "Text module with ID [ID] doesn't have gap with [INDEX] index or does not exists!"
            .replace('[ID]', decodedReference.moduleID)
            .replace('[INDEX]', decodedReference.gapIndex);
    }

    function isVariableEmpty(variable) {
        return variable === "" || variable === presenter.configuration.emptyAnswer;
    }

    presenter.getEmptyGaps = function (variables) {
        var emptyGaps = [], i, convertedVariable;

        for (i = 0; i &lt; variables.length; i++) {
            convertedVariable = presenter.convertVariable(variables[i].value, presenter.configuration.separators);
            if (convertedVariable === undefined) return { isValid: false, errorMessage: getAlertMessage(variables[i]) };

            if (isVariableEmpty(convertedVariable)) emptyGaps.push(variables[i].name);
        }

        return { isValid: true, gaps: emptyGaps };
    };

    presenter.getNotAttemptedGaps = function (variables) {
        var notAttemptedGaps = [], i, convertedVariable;

        for (i = 0; i &lt; variables.length; i++) {
            convertedVariable = presenter.isGapAttempted(variables[i].value);
            if (convertedVariable === undefined) return { isValid: false, errorMessage: getAlertMessage(variables[i]) };

            if (convertedVariable === false) notAttemptedGaps.push(variables[i].name);
        }

        return { isValid: true, gaps: notAttemptedGaps };
    };

    presenter.toggleAnswers = function (on, blockUnlocking = false) {
        presenter.isShowAnswers = on;
        for (var i=0; i&lt;presenter.configuration.answers.length; i++) {
            var answer = presenter.configuration.answers[i];
            var gapName = null;
            for (var j= 0; j&lt;presenter.configuration.variables.length; j++){
                if(presenter.configuration.variables[j].name == answer.name){
                    gapName = presenter.configuration.variables[j].value;
                }
            }
            if(gapName == null){
                return;
            }

            var moduleReference = presenter.decodeModuleReference(gapName);
            var module = presenter.getModule(moduleReference.moduleID);

            if (module != null &amp;&amp; !module.isActivity()) {
                if (on) {
                    answer.users = module.getValue(moduleReference.gapIndex);
                    module.setGapAnswer(moduleReference.gapIndex, answer.value, presenter.moduleAnswersCounter(moduleReference.moduleID));
                } else {
                    module.setUserValue(moduleReference.gapIndex, answer.users);
                    if (!blockUnlocking &amp;&amp; module.hasOwnProperty('enableGap')) module.enableGap(moduleReference.gapIndex);
                }
            }
        }

        presenter.reloadMathJax();
    }

    presenter.reloadMathJax = function() {
        const args = [];
        args.push("Typeset", MathJax.Hub);
        MathJax.Hub.Queue(args);
    }

    presenter.moduleAnswersCounter = function (module) {
        var counter = 0;

        for (var j= 0; j&lt;presenter.configuration.variables.length; j++){
            if(presenter.configuration.variables[j].value.indexOf(module) &gt; -1){
                counter++;
            }
        }

        return counter;
    };

    presenter.showAnswers = function() {
        if (!presenter.isShowAnswers) {
            presenter.toggleAnswers(true);
        }
    };

    presenter.hideAnswers = function() {
        if (presenter.isShowAnswers) {
            presenter.toggleAnswers(false);
        }
    };

    presenter.getActivitiesCount = function () {
        return presenter.configuration.answers.length;
    }

    presenter.gradualShowAnswers = function (data) {
        if (data.moduleID !== presenter.configuration.addonID) { return; }

        var answer = presenter.configuration.answers[presenter.currentGapIndex];
        var gapName = null;
        for (var j = 0; j &lt; presenter.configuration.variables.length; j++) {
            if (presenter.configuration.variables[j].name === answer.name) {
                gapName = presenter.configuration.variables[j].value;
            }
        }
        if (gapName == null) {
            return;
        }

        var moduleReference = presenter.decodeModuleReference(gapName);
        var module = presenter.getModule(moduleReference.moduleID);

        module.setGapAnswer(moduleReference.gapIndex, answer.value, presenter.moduleAnswersCounter(moduleReference.moduleID));
        presenter.currentGapIndex++;

        presenter.reloadMathJax();
    }

    presenter.allOkSent = false;

    presenter.onEventReceived = function(eventName, eventData) {
        switch(eventName) {
            case 'ShowAnswers': presenter.showAnswers(); break;
            case 'HideAnswers': presenter.hideAnswers(); break;
            case 'PageLoaded': markModules(); break;
            case 'ValueChanged':
                if (presenter.isAllOK() &amp;&amp; !presenter.allOkSent) {
                    presenter.allOkSent = true;
                    presenter.sendAllOKEvent();
                }else if(!presenter.isAllOK()){
                    presenter.allOkSent = false;
                }
                break;
            case 'GradualShowAnswers':
                presenter.gradualShowAnswers(eventData);
                break;
            case 'GradualHideAnswers':
                presenter.currentGapIndex = 0;
                presenter.hideAnswers();
                break;
        }
    };

    function markModules() {
        if(presenter.configuration.isError){
            return;
        }

        for (var i=0; i&lt;presenter.configuration.answers.length; i++) {
            var answer = presenter.configuration.answers[i];
            var moduleReference = presenter.decodeModuleReference(answer.name);
            var module = presenter.getModule(moduleReference.moduleID);

            if (module != null &amp;&amp; !module.isActivity()) {
                module.markConnectionWithMath();
            }
        }

        for (var j=0; j &lt; presenter.configuration.variables.length; j++){
            var decodedReference = presenter.decodeModuleReference(presenter.configuration.variables[j].value);
            var notSAmodule = presenter.getModule(decodedReference.moduleID);

            if (notSAmodule != null &amp;&amp; !notSAmodule.isActivity()) {
                notSAmodule.markConnectionWithMath();
            }
        }
    }

    presenter.printableController = null;
    presenter.setPrintableController = function(controller) {
        presenter.printableController = controller;
    }

    presenter.onBeforePrint = function(model) {
        if (!presenter.printableController) return;
        var upgradedModel = presenter.upgradeModel(model);
        var configuration = presenter.convertModel(upgradedModel);
        var result = presenter.evaluateAllExpressions(configuration.expressions, configuration.variables, configuration.separators, true);
        if (result.isError) return;
        var variableToGapDict = {};
        for (var i = 0; i &lt; configuration.variables.length; i++) {
            presenter.printableController.setCalculatedGapCorrect(configuration.variables[i].value, result.overall);
            variableToGapDict[configuration.variables[i].name] = configuration.variables[i].value;
        }
        for (var i = 0; i &lt; configuration.answers.length; i++) {
            var answer = configuration.answers[i];
            presenter.printableController.setCalculatedGapAnswer(variableToGapDict[answer.name], answer.value);
        }

    }

    presenter.isTableState = function(addonState) {
        return addonState.hasOwnProperty('gaps');
    }

    presenter.getGapValueFromTableState = function(gapIndex, addonState) {
        var gapState = addonState.gaps[gapIndex-1];
        if (gapState === undefined) return null;
        return gapState.value;
    }

    presenter.isTextState = function(addonState) {
        return addonState.hasOwnProperty('values') &amp;&amp; addonState.hasOwnProperty('gapUniqueId');
    }

    presenter.getGapValueFromTextState = function(gapIndex, addonState) {
        var values = JSON.parse(addonState.values);
        var gapKey = addonState.gapUniqueId + "-" + gapIndex;
        if (!values.hasOwnProperty(gapKey)) return null;
        return values[gapKey];
    }

    presenter.getVariableValueFromPrintableState = function(variableID) {
        if (!presenter.printableController) return null;
        var splitVariableID = variableID.split('.');
        if (splitVariableID.length !== 2) return null;
        var addonID = splitVariableID[0];
        var gapIndex = parseInt(splitVariableID[1]);
        if (isNaN(gapIndex)) return null;
        var addonStateRaw = presenter.printableController.getModuleState(addonID);
        if (!addonStateRaw) return null;
        var addonState = JSON.parse(addonStateRaw);
        var gapValue = null;
        if (presenter.isTableState(addonState)) {
            gapValue = presenter.getGapValueFromTableState(gapIndex, addonState);
        } else if (presenter.isTextState(addonState)) {
            gapValue = presenter.getGapValueFromTextState(gapIndex, addonState);
        }
        if (gapValue != null) {
            gapValue = gapValue.replace(',','.');
            if (isNaN(gapValue)) {
                gapValue = null;
            } else {
                gapValue = parseFloat(gapValue);
            }
        }
        return gapValue;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="MathText" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="initialText" nameLabel="Mathtext_property_text" type="mathtext"/>
        <property name="correctAnswer" nameLabel="Mathtext_property_correctAnswer" type="mathanswer"/>

        <property name="type" nameLabel="Mathtext_property_type" type="{text, editor, activity}"/>
        <property name="isDisabled" nameLabel="Mathtext_property_is_disabled" type="boolean"/>
        <property displayName="Language" name="language" nameLabel="Mathtext_property_language" type="{English, Polish, French, Arabic, Spanish}"/>

        <property name="formulaColor" nameLabel="Mathtext_property_formula_color" type="string"/>
        <property name="backgroundColor" nameLabel="Mathtext_property_background_color" type="string"/>

        <property name="mathEditorInPopup" nameLabel="Mathtext_property_math_editor_in_popup" type="boolean"/>
        <property displayName="popup texts" name="popupTexts" nameLabel="Mathtext_property_popup" type="staticlist">

        <property name="cancel" nameLabel="Mathtext_property_popup_button_save" type="staticrow">
                <property name="cancel" nameLabel="Mathtext_property_popup_button_cancel" type="string"/>
        </property>
        <property name="save" nameLabel="Mathtext_property_popup_button_save" type="staticrow">
                <property name="save" nameLabel="Mathtext_property_popup_button_save" type="string"/>
        </property>

        </property>
    </model>
<css>.mathtext-editor-wrapper {
    border: 1px solid black;
}

.mathtext-editor-wrapper.wrong {
    border: 2px solid red;
}

.mathtext-editor-wrapper.correct {
    border: 2px solid green;
}

.mathtext-editor-popup-wrapper {
    background: white;
    border: 1px solid;
    padding: 3px;
    width: 600px;
    position: absolute;
    display: none;
    top:0px;
    left:0px;
    padding-top:20px;
    z-index: 99999;
}
.mathtext-editor-popup-editor {
    width: 100%;
    height: 300px;
}

.mathtext-editor-popup-buttons {
    display: flex;
    padding-top: 5px;
}

.mathtext-editor-popup-buttons .cancel-button {
    display: inline-block;
    margin-left: auto;
    cursor: pointer;
}

.mathtext-editor-popup-buttons .save-button {
    display: inline-block;
    margin-left: 15px;
    cursor: pointer;
}
</css><view>&lt;div class="mathtext-editor-wrapper"&gt;
&lt;/div&gt;
&lt;img class="loading-image" style="position: absolute; visibility: hidden; margin: auto; left: 0; right: 0; top: 0; bottom: 0; max-width: 100%; max-height: 100%; object-fit: contain;"&gt;
&lt;/img&gt;
</view><preview>&lt;div class="mathtext-editor-wrapper" style="opacity: 0.999;"&gt;

&lt;/div&gt;
</preview><presenter>function AddonMathText_create() {
    var presenter = function() {};

    presenter.state = {
        isVisible: false,
        isDisabled: false,
        isShowAnswers: false,
        isCheckAnswers: false,
        currentAnswer: presenter.EMPTY_MATHTEXT,
        wasChanged: false, // for checking if user has made changes and needs update of scores
        lastScore: 0,
        hasUserInteracted: false // for checking if user has interacted with addon
    };
    presenter.editor = null;
    presenter.answerObject = null;
    presenter.waitingForChanges = false;

    presenter.TYPES_DEFINITIONS = {
        TEXT: 'text',
        EDITOR: 'editor',
        ACTIVITY: 'activity'
    };

    presenter.WIRIS_DISABLED_MESSAGE = "This addon requires internet access and enabled WIRIS to work correctly";

    presenter.EMPTY_MATHTEXT = '&lt;math xmlns="http://www.w3.org/1998/Math/MathML"/&gt;';
    presenter.WIRIS_RENDER_URL = "https://www.wiris.net/client/editor/render?";
    presenter.ERROR_CODES = {
        'initialText_STR02': "Value provided to text property is not a valid string.",
        'correctAnswer_STR02': "Value provided to text property is not a valid string.",
        'width_INT04': "Value provided to width property must be bigger than 500px.",
        'height_INT04': "Value provided to height property must be bigger than 200px.",
        "formulaColor_RGB01": "Formula color is not valid string.",
        "formulaColor_RGB02": "Formula color is too long for hex color.",
        "formulaColor_RGB03": "Formula color is not valid hex color.",
        "backgroundColor_RGB01": "Background color is not valid string.",
        "backgroundColor_RGB02": "Background color is too long for hex color.",
        "backgroundColor_RGB03": "Background color is not valid hex color."
    };

    presenter.run = function AddonMathText_run (view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.setPlayerController = function AddonMathText_setPlayerController (playerController) {
        presenter.playerController = playerController;
    };

    presenter.setEventBus = function (wrappedEventBus) {
        presenter.eventBus = wrappedEventBus;
    };

    presenter.createPreview = function AddonMathText_createPreview (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.makeRequestForImage = function AddonMathText_makeRequestForImage (text) {
        var xmlhttp = new XMLHttpRequest();
        var mathMlParam = encodeURIComponent(text);
        var imgTypeParam = 'svg';

        var url = presenter.WIRIS_RENDER_URL + "mml=" +  mathMlParam + "&amp;format=" + imgTypeParam;

        if (presenter.loadingImageView) {
            var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
            if (loadingSrc ) {
                presenter.loadingImageView.setAttribute("src", loadingSrc);
                presenter.loadingImageView.style.visibility = "visible";
            }
        }

        xmlhttp.onreadystatechange = function() {
            if (xmlhttp.readyState == XMLHttpRequest.DONE) {
                if (xmlhttp.status === 200) {
                    var sanitizedResponse = window.xssUtils.sanitize(xmlhttp.response);
                    $(presenter.wrapper).html(sanitizedResponse);
                } else {
                    $(presenter.wrapper).html(presenter.WIRIS_DISABLED_MESSAGE);
                }

                $(presenter.wrapper).css('color', presenter.configuration.formulaColor);
                $(presenter.wrapper).find('svg').css('fill', presenter.configuration.formulaColor);
                $(presenter.wrapper).css('background-color', presenter.configuration.backgroundColor);

                if (presenter.loadingImageView) {
                    presenter.loadingImageView.style.visibility = "hidden";
                }
            }
        };

        xmlhttp.open("GET", url, true);
        xmlhttp.send();
    };

    presenter.validateModel = function MathText_validateModel(model) {
        var modelValidator = new ModelValidator();

        var mathEditorInPopup = model["mathEditorInPopup"].toLowerCase() == "true";
        var hasMinSize = (model["type"] !== presenter.TYPES_DEFINITIONS.TEXT) &amp;&amp; !mathEditorInPopup;
        // when not showing wiris editor, width/height can be any value
        var widthConfig = new ModelValidators.utils.FieldConfigGenerator(function (validatedModel) {
            return {
                minValue: hasMinSize ? 500 : 0
            };
        });

        var heightConfig = new ModelValidators.utils.FieldConfigGenerator(function (validatedModel) {
            return {
                minValue: hasMinSize ? 200 : 0
            };
        });

        var availableLanugagesCodes = {
            'Polish': 'pl',
            'English': 'en',
            'Spanish': 'es',
            'Arabic': 'ar',
            'French': 'fr'
        };

        var validatedModel = modelValidator.validate(model, [
            ModelValidators.String("ID"),
            ModelValidators.Enum("type", {"default": "activity", values: ["text", "editor", "activity"]}),
            ModelValidators.Boolean("isDisabled"),
            ModelValidators.String("initialText", {default: presenter.EMPTY_MATHTEXT}),
            ModelValidators.String("correctAnswer", {default: presenter.EMPTY_MATHTEXT}),
            ModelValidators.utils.FieldRename("Is Visible", "isVisible", ModelValidators.Boolean("isVisible")),
            ModelValidators.utils.FieldRename("Width", "width", ModelValidators.Integer("width", widthConfig)),
            ModelValidators.utils.FieldRename("Height", "height", ModelValidators.Integer("height", heightConfig)),
            ModelValidators.utils.EnumChangeValues("language", availableLanugagesCodes, ModelValidators.Enum("language", {"default": "English", values: ["Polish", "English", "Spanish", "Arabic", "French"]})),
            ModelValidators.HEXColor("formulaColor", {"default": "#000000", canBeShort: true}),
            ModelValidators.HEXColor("backgroundColor", {"default": "#FFFFFF", canBeShort: false}),
            ModelValidators.Boolean("mathEditorInPopup")
        ]);

        if (!validatedModel.isValid) {
            return validatedModel;
        }

        presenter.setAdditionalConfigBasedOnType(validatedModel.value, validatedModel.value['type']);
        presenter.setButtonTextConfig(validatedModel.value, model);

        return validatedModel;
    };

    presenter.setAdditionalConfigBasedOnType = function(configuration, type) {
        // only when type is activity set isActivity to true - addon will return scores and react to commands like show/hide/check/uncheck answers
        if (type === presenter.TYPES_DEFINITIONS.TEXT) {
            configuration.isActivity = false;
            configuration.showEditor = false;
        } else if (type === presenter.TYPES_DEFINITIONS.EDITOR) {
            configuration.isActivity = false;
            configuration.showEditor = true;
        } else if (type === presenter.TYPES_DEFINITIONS.ACTIVITY){
            configuration.isActivity = true;
            configuration.showEditor = true;
        }
    };

    presenter.setButtonTextConfig = function(configuration, model) {
        var buttonTexts = {
            cancel: 'CANCEL',
            save: 'SAVE'
        }
        if (model['popupTexts']) {
            var cancelText = model['popupTexts']['cancel']['cancel'];
            if (cancelText &amp;&amp; cancelText.trim().length &gt; 0) {
                buttonTexts.cancel = cancelText;
            }
            var saveText = model['popupTexts']['save']['save'];
            if (saveText &amp;&amp; saveText.trim().length &gt; 0) {
                buttonTexts.save = saveText;
            }
        }
        configuration.buttonTexts = buttonTexts;
    }

    presenter.upgradeModel = function(model) {
        return presenter.addMathEditorInPopup(model);
    }

    presenter.addMathEditorInPopup = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel['mathEditorInPopup'] === undefined) {
            upgradedModel['mathEditorInPopup'] = "False";
        }

        if (upgradedModel['popupTexts'] === undefined) {
            upgradedModel['popupTexts'] = {
                cancel: {cancel: ''},
                save: {save: ''}
            }
        }

        return upgradedModel;
    }

    presenter.presenterLogic = function AddonMathText_presenterLogic (view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.wrapper = presenter.view.getElementsByClassName('mathtext-editor-wrapper')[0];
        presenter.loadingImageView = presenter.view.getElementsByClassName('loading-image')[0];
        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);

        if (!validatedModel.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.fieldName.join("|") + "_" + validatedModel.errorCode);
            return;
        }
        presenter.configuration = validatedModel.value;

        presenter.initializeView(isPreview);

        // when preview, addon always should be visible, when in lesson it should depend on configuration
        presenter.setVisibility(isPreview || presenter.configuration.isVisible);
        presenter.setDisabled(presenter.configuration.isDisabled);

        if (!isPreview) {
            presenter.addHandlers();
        }

        // added in preview too, so it won't slow editor down
        if (presenter.configuration.showEditor) {
            MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }
        presenter.waitingForChanges = true;
    };

    presenter.initializeView = function AddonMathText_initializeView (isPreview) {
        presenter.wrapper.style.width = presenter.configuration.width + 'px';
        presenter.wrapper.style.height = presenter.configuration.height + 'px';


        if (presenter.configuration.showEditor) {
            if (presenter.configuration.mathEditorInPopup) {
                presenter.initializePopupEditor(isPreview);
            } else {
                presenter.initializeEditor(isPreview);
            }
        } else {
            presenter.initializeText();
        }
    };

    presenter.initializeText = function AddonMathText_initializeText() {
        presenter.makeRequestForImage(presenter.configuration.initialText);
    };

    presenter.initializationState = function AddonMathText_initializationState () {
        presenter.state.isVisible = presenter.configuration.isVisible;
    };

    presenter.addHandlers = function AddonMathText_addHandlers () {
        if (presenter.eventBus) {
            presenter.eventBus.addEventListener('ShowAnswers', this);
            presenter.eventBus.addEventListener('HideAnswers', this);
        }

        if (!presenter.isWirisEnabled()) return;

        if (presenter.configuration.isActivity) {
            presenter.editorListener = {
                caretPositionChanged: function () {},
                clipboardChanged: function () {},
                styleChanged: function () {},
                transformationReceived: function () {},
                contentChanged: function (editor) {
                    // comparing texts fixes problem with userInteraction and hideAnswer
                    // when using setMathML WIRIS editor needs to make request to WIRIS API,
                    // so this callback will be called asynchronusly after request response,
                    // but after state.isShowAnswers was already changed in normal execution

                    const currentText = editor.getMathML();
                    if (presenter.editor.isReady() &amp;&amp;
                        presenter.waitingForChanges &amp;&amp;
                        currentText !== presenter.state.currentAnswer
                    ) {
                        presenter.state.wasChanged = true;
                        presenter.state.hasUserInteracted = true;
                    }
                }
            };

            presenter.editor.getEditorModel().addEditorListener(presenter.editorListener);
        }
    };

    presenter.destroy = function AddonMathText_removeHandler (event) {
        if (event.target !== presenter.view) {
            return;
        }

        $(window).off("pointerdown", presenter.onFocusOutsideEditor);

        if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
            presenter.editor.getEditorModel().removeEditorListener(presenter.editorListener);
            presenter.removeWIRISEditor();
        }
    };

    presenter.removeWIRISEditor = function () {
        window.com.wiris.jsEditor.JsEditor.removeInstance(presenter.wrapper);
    };

    presenter.getWIRISEditor = function() {
        return window.com.wiris.jsEditor.JsEditor.getInstance(presenter.wrapper);
    };

    presenter.isWirisEnabled = function() {
        return window.hasOwnProperty('com') &amp;&amp; window.com.hasOwnProperty('wiris') &amp;&amp; window.com.wiris.hasOwnProperty('jsEditor');
    };

    presenter.initializeEditor = function AddonMathText_initializeEditor (isPreview) {
        if (!presenter.isWirisEnabled()) {
            $(presenter.wrapper).html(presenter.WIRIS_DISABLED_MESSAGE);
            return;
        }

        presenter.editor = createWirisEditor();
        presenter.editor.insertInto(presenter.wrapper);

        if (isPreview) {
            presenter.$view.find('.wrs_focusElementContainer').css('display', 'none');
            presenter.$view.find('.wrs_handWrapper').css('display', 'none');
        } else {
            const builder = window.com.wiris.quizzes.api.QuizzesBuilder.getInstance();
            presenter.answerObject = builder.readQuestion(presenter.configuration.correctAnswer);

            // when initial text is correct answer
            presenter.calculateScore(presenter.configuration.initialText);
            presenter.wrapper.addEventListener('focusin', presenter.onFocusInEditor);
            $(window).on("pointerdown", presenter.onFocusOutsideEditor);
        }
    };

    presenter.initializePopupEditor = function AddonMathText_initializePopupEditor (isPreview) {
        if (!presenter.isWirisEnabled()) {
            $(presenter.wrapper).html(presenter.WIRIS_DISABLED_MESSAGE);
            return;
        }

        presenter.createWirisPopup(isPreview);
        presenter.editor.insertInto(presenter.editorPopupEditor);
        presenter.wrapper.addEventListener('click', presenter.showPopup);

        if (!isPreview) {
            presenter.resetPopupPosition();
            const builder = window.com.wiris.quizzes.api.QuizzesBuilder.getInstance();
            presenter.answerObject = builder.readQuestion(presenter.configuration.correctAnswer);
            presenter.calculateScore(presenter.configuration.initialText);
        }
        presenter.makeRequestForImage(presenter.configuration.initialText);
    };

    presenter.createWirisPopup = function AddonMathText_createWirisPopup (isPreview) {
        presenter.editor = createWirisEditor();

        presenter.editorPopupWrapper = document.createElement('div');
        presenter.editorPopupWrapper.classList.add('mathtext-editor-popup-wrapper');

        presenter.editorPopupEditor = document.createElement('div');
        presenter.editorPopupEditor.classList.add('mathtext-editor-popup-editor');

        presenter.editorPopupCancel = document.createElement('div');
        presenter.editorPopupCancel.classList.add('cancel-button');
        presenter.editorPopupCancel.innerText = presenter.configuration.buttonTexts.cancel;

        presenter.editorPopupSave = document.createElement('div');
        presenter.editorPopupSave.classList.add('save-button');
        presenter.editorPopupSave.innerText = presenter.configuration.buttonTexts.save;

        const editorPopupButtons = document.createElement('div');
        editorPopupButtons.classList.add('mathtext-editor-popup-buttons');
        editorPopupButtons.appendChild(presenter.editorPopupCancel);
        editorPopupButtons.appendChild(presenter.editorPopupSave);

        presenter.editorPopupSave.addEventListener('click', presenter.onSavePopup);
        presenter.editorPopupCancel.addEventListener('click', presenter.onCancelPopup);

        presenter.editorPopupWrapper.appendChild(presenter.editorPopupEditor);
        presenter.editorPopupWrapper.appendChild(editorPopupButtons);
        presenter.view.appendChild(presenter.editorPopupWrapper);
        $(presenter.editorPopupWrapper).draggable({
            cancel: '.mathtext-editor-popup-editor,.cancel-button,.save-button'
        });
    }

    function createWirisEditor(isPreview) {
        return window.com.wiris.jsEditor.JsEditor.newInstance(
            {
                'language': presenter.configuration.language,
                'mml': presenter.configuration.initialText,
                'readOnly': isPreview,
                'color': presenter.configuration.formulaColor,
                'backgroundColor': presenter.configuration.backgroundColor
            }
        );
    }

    presenter.onSavePopup = function AddonMathText_onSavePopup () {
        presenter.state.currentAnswer = presenter.editor.getMathML();
        presenter.calculateScore(presenter.state.currentAnswer);
        presenter.makeRequestForImage(presenter.state.currentAnswer);
        presenter.hidePopup();
        presenter.resetPopupPosition();
        presenter.sendSavedEvent();
    }

     presenter.onCancelPopup = function AddonMathText_onCancelPopup () {
        if (presenter.state.currentAnswer &amp;&amp; presenter.state.currentAnswer !== presenter.EMPTY_MATHTEXT) {
            presenter.setMathML(presenter.state.currentAnswer);
        } else {
            presenter.setMathML(presenter.configuration.initialText);
        }
        presenter.hidePopup();
        presenter.resetPopupPosition();
        presenter.sendCanceledEvent();
    }

    presenter.resetPopupPosition = function AddonMathText_resetPopupPosition () {
        presenter.editorPopupWrapper.style.visibility = 'hidden';
        var oldDisplayValue = presenter.editorPopupWrapper.style.display;
        presenter.editorPopupWrapper.style.display = 'block';
        var pageElement = $(presenter.view).closest('.ic_page_panel')[0];
        var offsetLeft = Math.floor((pageElement.offsetWidth - presenter.editorPopupWrapper.offsetWidth)/2) - presenter.view.offsetLeft;
        presenter.editorPopupWrapper.style.left = offsetLeft + "px";
        presenter.editorPopupWrapper.style.top = '';
        presenter.editorPopupWrapper.style.display = oldDisplayValue;
        presenter.editorPopupWrapper.style.visibility = '';
    }

    presenter.showPopup = function AddonMathText_showPopup () {
        if (!presenter.state.isShowAnswers &amp;&amp; !presenter.state.isCheckAnswers &amp;&amp; !presenter.state.isDisabled) {
            presenter.editorPopupWrapper.style.display = "block";
        }
    }

    presenter.hidePopup = function AddonMathText_hidePopup () {
        presenter.editorPopupWrapper.style.display = "none";
    }

    presenter.showAnswers = function AddonMathText_showAnswers () {
        if (presenter.state.isCheckAnswers) {
            presenter.setWorkMode();
        }

        if (presenter.configuration.showEditor &amp;&amp; !presenter.state.isShowAnswers &amp;&amp; presenter.isWirisEnabled()) {
            presenter.waitingForChanges = false;
            presenter.state.isShowAnswers = true;
            presenter.$view.find('input').attr('disabled', true);
            presenter.editor.setToolbarHidden(true);

            if (presenter.configuration.isActivity) {
                if (presenter.configuration.mathEditorInPopup) {
                    presenter.hidePopup();
                    presenter.makeRequestForImage(presenter.answerObject.getCorrectAnswer(0));
                } else {
                    presenter.state.currentAnswer = presenter.editor.getMathML();
                    presenter.setMathML(presenter.answerObject.getCorrectAnswer(0));
                }
            }
        }
    };

    presenter.hideAnswers = function AddonMathText_hideAnswers () {
        if (presenter.configuration.showEditor &amp;&amp; presenter.state.isShowAnswers &amp;&amp; presenter.isWirisEnabled()) {
            presenter.editor.setToolbarHidden(false);
            presenter.$view.find('input').removeAttr('disabled');

            if (presenter.configuration.isActivity) {
                if (presenter.configuration.mathEditorInPopup) {
                    if (!presenter.state.currentAnswer || presenter.state.currentAnswer === presenter.EMPTY_MATHTEXT) {
                        presenter.makeRequestForImage(presenter.configuration.initialText);
                    } else {
                        presenter.makeRequestForImage(presenter.state.currentAnswer);
                    }
                } else {
                    presenter.setMathML(presenter.state.currentAnswer);
                }
            }

            presenter.state.isShowAnswers = false;
            presenter.waitingForChanges = true;
            presenter.setDisabled(presenter.state.isDisabled);
        }
    };

    presenter.setShowErrorsMode = function AddonMathText_setShowErrorsMode () {
        if (presenter.state.isShowAnswers) {
            presenter.hideAnswers();
        }

        if (presenter.configuration.showEditor &amp;&amp; !presenter.state.isCheckAnswers &amp;&amp; presenter.isWirisEnabled()) {
            presenter.waitingForChanges = false;
            presenter.state.isCheckAnswers = true;
            presenter.$view.find('input').attr('disabled', true);
            presenter.editor.setToolbarHidden(true);

            if (presenter.configuration.isActivity) {

                if (!presenter.configuration.mathEditorInPopup) {
                    presenter.state.currentAnswer = presenter.editor.getMathML();
                }

                if (presenter.state.hasUserInteracted) {
                    const score = presenter.getScore();

                    if (score === 1) {
                        presenter.wrapper.classList.add('correct');
                    } else {
                        presenter.wrapper.classList.add('wrong');
                    }
                }
            }
        }
    };

    presenter.setWorkMode = function AddonMathText_setWorkMode () {
        if (presenter.state.isCheckAnswers) {
            presenter.wrapper.classList.remove('correct');
            presenter.wrapper.classList.remove('wrong');

            if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
                presenter.$view.find('input').removeAttr('disabled');
                presenter.editor.setToolbarHidden(false);
            }

            presenter.state.isCheckAnswers = false;
            presenter.setDisabled(presenter.state.isDisabled);
            presenter.waitingForChanges = true;
        }
    };

    presenter.reset = function () {
        presenter.state.isCheckAnswers = false;
        presenter.state.isShowAnswers = false;

        presenter.wrapper.classList.remove('correct');
        presenter.wrapper.classList.remove('wrong');

        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.setDisabled(presenter.configuration.isDisabled);

        presenter.state.wasChanged = false;
        presenter.state.hasUserInteracted = false;

        if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
            presenter.state.currentAnswer = presenter.configuration.initialText;
            presenter.setMathML(presenter.configuration.initialText);
            presenter.calculateScore(presenter.configuration.initialText)
            if (presenter.configuration.mathEditorInPopup) {
                presenter.makeRequestForImage(presenter.configuration.initialText);
            }
        }
    };

    presenter.onEventReceived = function AddonMathText_onEventReceived (eventName) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        }
        else if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.calculateScore = function (text) {
        presenter.state.lastScore = presenter.checkIfAnswerIsCorrect(text) ? 1 : 0;
    };

    presenter.checkIfAnswerIsCorrect = function(userText) {
        if (!presenter.configuration.isActivity) {
            return false;
        }

        // special case - user defined empty text as correct answer and userText is string equal to empty mathml
        var correctAnswerText = presenter.answerObject.getCorrectAnswer(0);
        if (correctAnswerText === '' &amp;&amp; userText === presenter.EMPTY_MATHTEXT) {
            return true;
        } else if (correctAnswerText === null) { // correct answer isn't defined
            return false;
        }

        return presenter.requestForQuizzesCorrectness(correctAnswerText, userText);
    };

    presenter.requestForQuizzesCorrectness = function (correctAnwser, userText) {
        var builder = window.com.wiris.quizzes.api.QuizzesBuilder.getInstance();

        var request = builder.newEvalRequest(correctAnwser, userText, presenter.answerObject, null);
        var service = builder.getQuizzesService();
        var response = service.execute(request);
        // Get the response into this useful object.
        var instance = builder.newQuestionInstance();
        instance.update(response);
        // Ask for the correctness of the 0th response.
        return instance.isAnswerCorrect(0);
    };

    presenter.setVisibility = function AddonMathText_setVisibility (isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.setDisabled = function(value) {
        presenter.state.isDisabled = value;

        // that means input is disabled, and toolbar is hidden
        if (presenter.state.isShowAnswers || presenter.state.isCheckAnswers) {
            return;
        }

        if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
            if (value) {
                presenter.$view.find('input').attr('disabled', true);
            } else {
                presenter.$view.find('input').removeAttr('disabled');
            }

            presenter.editor.setToolbarHidden(value);
        }
    };

    presenter.setState = function (state) {
        const parsedState = JSON.parse(state);
        if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
            presenter.setMathML(parsedState.text);
            presenter.state.currentAnswer = parsedState.text;
            presenter.calculateScore(presenter.state.currentAnswer);
            if (presenter.configuration.mathEditorInPopup) {
                presenter.makeRequestForImage(parsedState.text);
            }
        }

        presenter.state.hasUserInteracted = parsedState.hasUserInteracted;
        presenter.setVisibility(parsedState.isVisible);
        presenter.setDisabled(parsedState.isDisabled)
    };


    presenter.getState = function() {
        var currentText = presenter.configuration.initialText;

        if (presenter.configuration.showEditor &amp;&amp; presenter.isWirisEnabled()) {
            if (!presenter.configuration.mathEditorInPopup) {
                currentText = presenter.editor.getMathML();
            } else {
                if (presenter.state.currentAnswer) {
                    currentText = presenter.state.currentAnswer;
                }
            }
        }

        return JSON.stringify({
            'text': currentText,
            'isVisible': presenter.state.isVisible,
            'hasUserInteracted': presenter.state.hasUserInteracted,
            'isDisabled': presenter.state.isDisabled
        })
    };

    presenter.getScore = function() {
        if (!presenter.configuration.isActivity || !presenter.isWirisEnabled()) {
            return 0;
        }
        if (presenter.state.wasChanged &amp;&amp; !presenter.state.isShowAnswers &amp;&amp; presenter.editor.isHandOpen()) {
            presenter.calculateScore(presenter.editor.getMathML());
        }
        return presenter.state.lastScore;
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isActivity &amp;&amp; presenter.state.hasUserInteracted) {
            return 1 - presenter.getScore();
        }
        return 0;
    };

    presenter.getMaxScore = function () {
        return presenter.configuration.isActivity ? 1 : 0;
    };

    presenter.sendSavedEvent = function () {
        const eventData = {
            source: presenter.configuration.ID,
            item: 'saved',
            value: 1,
            score: presenter.state.lastScore
        };

        presenter.sendValueChangedEvent(eventData);
    };

    presenter.sendCanceledEvent = function () {
        const eventData = {
            source: presenter.configuration.ID,
            item: 'canceled',
            value: 1,
            score: presenter.state.lastScore
        };

        presenter.sendValueChangedEvent(eventData);
    };

    presenter.onFocusInEditor = function (event) {
        presenter.hasEditorFocus = true;
    };

    presenter.onFocusOutsideEditor = function (event) {
        if (!presenter.hasEditorFocus) {
            return;
        }

        if (presenter.wrapper.contains(event.target)) {
            return;
        }
        presenter.hasEditorFocus = false;
        if (presenter.state.wasChanged) {
            presenter.calculateScore(presenter.editor.getMathML());
        }

        const eventData = {
            source: presenter.configuration.ID,
            item: 'blur',
            value: 1,
            score: presenter.state.lastScore
        };

        presenter.sendValueChangedEvent(eventData);
    };

    presenter.sendValueChangedEvent = function (eventData) {
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.disable = function() {
        presenter.setDisabled(true);
    };

    presenter.enable = function() {
        presenter.setDisabled(false);
    };

    presenter.executeCommand = function(name, params) {
        const commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'enable': presenter.enable,
            'disable': presenter.disable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setMathML = function (mathml) {
        presenter.waitingForChanges = false;
        presenter.editor.setMathMLWithCallback(mathml, () =&gt; {
            presenter.waitingForChanges = true;
        });
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Maze" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="mazeWidth" nameLabel="Maze_property_width" type="string"/>
		<property name="mazeHeight" nameLabel="Maze_property_height" type="string"/>
		<property name="numberOfMazes" nameLabel="Maze_property_number_of_mazes" type="string"/>
		<property name="gameMode" nameLabel="Maze_property_game_mode" type="{Doors,Letters}"/>

		<property name="questions" nameLabel="Maze_property_questions" type="list">
            <property name="question" nameLabel="Maze_property_questions_question" type="html"/>
            <property name="answer" nameLabel="Maze_property_questions_answer" type="string"/>
			<property name="letter" nameLabel="Maze_property_questions_letter" type="string"/>
			<property name="mazeId" nameLabel="Maze_property_questions_maze_number" type="string"/>
			<property name="isCaseSensitive" nameLabel="Maze_property_questions_is_case_sensitive" type="boolean"/>
        </property>
		<property name="isDisabled" nameLabel="Maze_property_is_disabled" type="boolean"/>
		<property name="hideControlPanel" nameLabel="Maze_property_hide_control_panel" type="boolean"/>
		<property name="translations" nameLabel="Maze_property_translations" type="staticlist">
			<property name="endGame" nameLabel="Maze_property_translations_end_game" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="applyQuestion" nameLabel="Maze_property_translations_apply_question_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="nextMazeButton" nameLabel="Maze_property_translations_next_maze_button" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="solutionText" nameLabel="Maze_property_translations_level_solution_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="upButton" nameLabel="Maze_property_translations_up_button_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="leftButton" nameLabel="Maze_property_translations_left_button_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="rightButton" nameLabel="Maze_property_translations_right_button_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="downButton" nameLabel="Maze_property_translations_down_button_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="loose" nameLabel="Maze_property_translations_loose_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
			<property name="looseButton" nameLabel="Maze_property_translations_retry_button_text" type="staticrow">
				<property name="translation" nameLabel="Maze_property_translations_translation" type="string"/>
			</property>
		</property>
	</model>
<css>.Maze-wrapper {
    width: 100%;
    height: 100%;
}

.Maze-wrapper-menu {
    float: right;
    height: 100%;
    width: 200px;
    border-left: solid 1px;
    box-sizing: border-box;
}

.Maze-wrapper-game-container{
    float: left;
    height: 100%;
    width: calc(100% - 200px);
}

.Maze-wrapper-menu-controls {
    position: absolute;
    bottom: 0;
    width: inherit;
}

.Maze-wrapper-menu-controls-up {
    width: 33%;
    margin-left: auto;
    margin-right: auto;
}
.Maze-wrapper-menu-controls-down {
    clear: both;
    width: 33%;
    margin-left: auto;
    margin-right: auto;
}

.Maze-wrapper-menu-controls-left {
    float: left;
    width: 33%;
}

.Maze-wrapper-menu-controls-right {
    float: right;
    width: 33%;
}

.Maze-wrapper-menu-controls-no-select {
    -webkit-touch-callout: none; /* iOS Safari */
    -webkit-user-select: none; /* Safari */
    -khtml-user-select: none; /* Konqueror HTML */
    -moz-user-select: none; /* Firefox */
    -ms-user-select: none; /* Internet Explorer/Edge */
    user-select: none; /* Non-prefixed version, currently
                          supported by Chrome and Opera */
}

.Maze-wrapper-menu-controls-up-text {
    text-align: center;
}

.Maze-wrapper-menu-controls-down-text {
    text-align: center;
}

.Maze-wrapper-menu-controls-left-text {
    text-align: center;
}

.Maze-wrapper-menu-controls-right-text {
    text-align: center;
}

.Maze_main_container {
    width: 100%;
    height: 100%;
}

.Maze_room {
    float: left;
    -webkit-transform: rotate(0deg);
    -moz-transform: rotate(0deg);
    -ms-transform: rotate(0deg);
    -o-transform: rotate(0deg);
    transform: rotate(0deg);
    background-color: white;
}

.Maze_room &gt; div {
    width: 100%;
    height:100%;
    position: absolute;
}

.Maze_room_image_top_left_right_bottom {
    background: url('resources/topleftrightbottom.svg') no-repeat center;
    background-size: contain;    
}

.Maze_room_image_top_left_right {
    background: url('resources/topleftright.svg') no-repeat center;
    background-size: contain;  
}

.Maze_room_image_top_left {
    background: url('resources/topleft.svg') no-repeat center;
    background-size: contain;  
}

.Maze_room_image_top_bottom {
    background: url('resources/topbottom.svg') no-repeat center;
    background-size: contain;  
}

.Maze_room_image_top {
    background: url('resources/top.svg') no-repeat center;
    background-size: contain;  
}

.Maze_player_element {
    background: url('resources/player.svg') no-repeat center;
    background-size: contain;  
}

.Maze_treasure_element {
    background: url('resources/treasure.svg') no-repeat center;
    background-size: 80% 80%;
}

.Maze_door {
    background: url('resources/door.svg') no-repeat center;
    background-size: 80% 80%;
}

.Maze_door_opened {
    background: url('resources/door_opened.svg') no-repeat center;
    background-size: 80% 80%;
}

.Maze_room_left_top_dot {
    background: url('resources/dot.svg') no-repeat center;
    background-size: contain;   
}

.Maze_game_question_background {
    background-color: lightgray;
    width: 100%;
    height: 100%;
    position: absolute;
    opacity: 0.6;
    left: -1px;
    top: -1px;
    display: none;
}

.Maze_game_question_container {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    width: 100%;
    display: none;
}

.Maze_game_question_container_question_input {
    width: 100%;
    box-sizing: border-box;
}

.Maze_game_question_container_question_apply_text {
    text-align: center;
}

.Maze_game_question_container_question_apply {
    background-color: gray;
    margin-top: 5px;

}

.Maze_game_question_container_question_apply:hover {
    background-color: lightgray;
}

.Maze_game_end {
    width: 100%;
    height: 100%;
    position: absolute;
    top: -1px;
    left: -1px;
    background-color: gray;
    display: none;
}

.Maze_loose {
    width: 100%;
    height: 100%;
    position: absolute;
    top: -1px;
    left: -1px;
    background-color: gray;
    display: none;
}

.Maze_game_loose_wrapper {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    width: 100%;
}

.Maze_game_loose_text {
    width: 100%;
    text-align: center;
}

.Maze_game_loose_button {
    width: 100%;
    background-color: lightgray;
    text-align: center;
    margin-top: 50px;
}

.Maze_game_end_text {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    width: 100%;
    text-align: center;
}

.Maze_letters_end_level_answer_letters_end_game {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    width: 100%;
    text-align: center;
}

.Maze_letters_end_level {
    background: url('resources/letterEndGame.svg') no-repeat center;
    background-size: 80% 80%;
}

.Maze_letters_end_level_background {
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: gray;
    display: none;
}

.Maze_letters_end_level_next_maze_button {
    width: 100%;
    background-color: lightgray;
    text-align: center;
    margin-top: 50px;
}

.Maze_end_level_answer_end_game {
    text-align: center;
}

.Maze_letters_end_level_answer_wrapper {
    position: absolute;
    width: 100%;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    display: none;
}

.Maze_letters_letter_element {
    float: left;
    width: 20px;
    border: 1px black solid;
    text-align: center;
}

.Maze_levelSolutionText {
    float: left;
    margin-right: 5px;
}

.Maze_letters_end_level_answer_container {
     position: absolute;
    left: 50%;
    -webkit-transform: translateX(-50%);
    -ms-transform: translateX(-50%);
    transform: translateX(-50%);
}

.Maze_letters_room_letter {
    width: 100%;
    text-align: center;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    -webkit-transform: translateY(-50%);
    -ms-transform: translateY(-50%);
    font-size: 1em;
}
</css><view>&lt;div class="Maze-wrapper" tabindex="0"&gt;
  &lt;div class="Maze-wrapper-game-container"&gt;

  &lt;/div&gt;
  &lt;div class="Maze-wrapper-menu"&gt;
    &lt;div class="Maze-wrapper-menu-state-container"&gt;
    &lt;/div&gt;
    &lt;div class="Maze-wrapper-menu-controls"&gt;
      &lt;div class="Maze-wrapper-menu-controls-up"&gt;
        &lt;div class="Maze-wrapper-menu-controls-up-text Maze-wrapper-menu-controls-no-select"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-left"&gt;
        &lt;div class="Maze-wrapper-menu-controls-left-text Maze-wrapper-menu-controls-no-select"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-right"&gt;
        &lt;div class="Maze-wrapper-menu-controls-right-text Maze-wrapper-menu-controls-no-select"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-down"&gt;
        &lt;div class="Maze-wrapper-menu-controls-down-text Maze-wrapper-menu-controls-no-select"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="Maze_game_question_background"&gt;&lt;/div&gt;
  &lt;div class="Maze_game_question_container"&gt;
    &lt;div class="Maze_game_question_container_question_text"&gt;&lt;/div&gt;
    &lt;input class="Maze_game_question_container_question_input"/&gt;
    &lt;div class="Maze_game_question_container_question_apply"&gt;
      &lt;div class="Maze_game_question_container_question_apply_text"&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="Maze_loose"&gt;
      &lt;div class="Maze_game_loose_wrapper"&gt;
        &lt;div class="Maze_game_loose_text"&gt;&lt;/div&gt;
        &lt;div class="Maze_game_loose_button"&gt;&lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="Maze_game_end"&gt;
    &lt;div class="Maze_game_end_text"&gt;&lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="Maze_letters_end_level_background"&gt;&lt;/div&gt;
  &lt;div class="Maze_letters_end_level_answer_wrapper"&gt;
    &lt;div class="Maze_letters_end_level_answer_container"&gt;
      &lt;div class="Maze_end_level_answer_end_game"&gt;
      &lt;/div&gt;
      &lt;div class="Maze_letters_end_level_answer_letters_container"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="Maze_letters_end_level_next_maze_button"&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="Maze-wrapper"&gt;
  &lt;div class="Maze-wrapper-game-container"&gt;

  &lt;/div&gt;
  &lt;div class="Maze-wrapper-menu"&gt;
    &lt;div class="Maze-wrapper-menu-state-container"&gt;
    &lt;/div&gt;
    &lt;div class="Maze-wrapper-menu-controls"&gt;
      &lt;div class="Maze-wrapper-menu-controls-up"&gt;
        &lt;div class="Maze-wrapper-menu-controls-up-text"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-left"&gt;
        &lt;div class="Maze-wrapper-menu-controls-left-text"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-right"&gt;
        &lt;div class="Maze-wrapper-menu-controls-right-text"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class="Maze-wrapper-menu-controls-down"&gt;
        &lt;div class="Maze-wrapper-menu-controls-down-text"&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonMaze_create () {
    var presenter = function () {};

    presenter.ERROR_MESSAGES = {
        'mazeWidth_INT01': 'Width can\'t be empty',
        'mazeWidth_INT02': 'Width must be integer',
        'mazeWidth_INT04': 'Width must be bigger than 5',
        'mazeHeight_INT01': 'Height can\'t be empty',
        'mazeHeight_INT02': 'Height must be integer',
        'mazeHeight_INT04': 'Height must be bigger than 5',
        'numberOfMazes_INT01': 'Number of mazes can\'t be empty',
        'numberOfMazes_INT02': 'Number of mazes must be integer',
        'numberOfMazes_INT04': 'Number of mazes must be positive integer',
        'questions_mazeId_INT02': 'Maze id must be integer',
        'questions_mazeId_INT04': 'Maze id must be positive integer',
        'questions_letter_STR01': 'Maze letter can\'t be empty'
    };

    function escape(unsafe) {
        return unsafe.replace(/[&amp;&lt;"']/g, function (m) {
            switch (m) {
                case '&amp;':
                    return '&amp;amp;';
                case '&lt;':
                    return '&amp;lt;';
                case '"':
                    return '&amp;quot;';
                default:
                    // apostrophe character
                    return '&amp;#039;';
            }
        });
    }

    presenter.state = {
        games: [],
        actualGameIndex: 0,
        mistakes: 0,
        errorCount: 0,
        applyButtonClickCallback: function () {},

        elements: {
            questionContainer: null,
            questionBackground: null,
            gameContainer: null,
            upButton: null,
            leftButton: null,
            rightButton: null,
            downButton: null,
            applyButton: null,
            questionText: null,
            answerInput: null,
            applyButtonText: null,
            endGameButtonText: null,
            endGame: null,
            menu: null,
            lettersAnswerBackground: null,
            lettersAnswerContainer: null,
            lettersAnswerButton: null,
            lettersContainer: null,
            lettersEndGameText: null,
            looseText: null,
            looseRetryButton: null,
            looseWrapper: null,
            addonWrapper: null
        },

        isDisabled: false,
        nextMazeButtonCallback: function () {},
        isShowingAnswers: false,
        isShowingErrors: false,
        _isDisabled: false,  // is while showing question,
        isVisible: false

    };

    presenter.configuration = {};

    presenter.GAME_TYPES = {
        'DOORS': 1,
        'LETTERS': 2
    };

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer,
            key;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }
        presenter.$view.html(errorContainer);
    };

    presenter.run = function(view, model) {
        presenter.runLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.runLogic(view, model, true);
    };

    presenter.getScore = function () {
        var actualGameScore = 0;
        if (presenter.getActualGame()) {
            actualGameScore = presenter.getActualGame().getScore();
        }
        return presenter.state.actualGameIndex + actualGameScore;
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isValid) {
            return presenter.configuration.numberOfMazes;
        } else {
            return 0;
        }
    };

    presenter.getErrorCount = function () {
        var lastErrorCount = presenter.state.errorCount;
        presenter.state.errorCount = 0;

        return lastErrorCount;
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) { return; }

        if (presenter.getActualGame()) {
            presenter.getActualGame().destroy();
        }
        presenter.disconnectHandlers();
    };

    /**
     * Set in state expected elements like DOM elements
     */
    presenter.completeState = function () {
        var expectedElements = {
                questionContainer: 'Maze_game_question_container',
                questionBackground: 'Maze_game_question_background',
                gameContainer: 'Maze-wrapper-game-container',
                upButton: 'Maze-wrapper-menu-controls-up',
                leftButton: 'Maze-wrapper-menu-controls-left',
                rightButton: 'Maze-wrapper-menu-controls-right',
                downButton: 'Maze-wrapper-menu-controls-down',
                applyButton: 'Maze_game_question_container_question_apply',
                applyButtonText: 'Maze_game_question_container_question_apply_text',
                questionText: 'Maze_game_question_container_question_text',
                answerInput: 'Maze_game_question_container_question_input',
                endGame: 'Maze_game_end',
                endGameButtonText: 'Maze_game_end_text',
                menu: 'Maze-wrapper-menu',
                lettersAnswerBackground: 'Maze_letters_end_level_background',
                lettersAnswerContainer: 'Maze_letters_end_level_answer_wrapper',
                lettersAnswerButton: 'Maze_letters_end_level_next_maze_button',
                lettersContainer: 'Maze_letters_end_level_answer_letters_container',
                lettersEndGameText: 'Maze_end_level_answer_end_game',
                looseText: 'Maze_game_loose_text',
                looseRetryButton :'Maze_game_loose_button',
                looseWrapper: 'Maze_loose',
                addonWrapper: 'Maze-wrapper'
            },
            i;

        for (i in expectedElements) {
            if (expectedElements.hasOwnProperty(i)) {
                presenter.state.elements[i] = presenter.view.getElementsByClassName(expectedElements[i])[0];
            }
        }

        presenter.state.isDisabled = presenter.configuration.isDisabled;
    };

    function rotateElement (element, rotation) {
        element.style.webkitTransform = 'rotate(' + rotation + 'deg)';
        element.style.mozTransform    = 'rotate(' + rotation + 'deg)';
        element.style.msTransform     = 'rotate(' + rotation + 'deg)';
        element.style.oTransform      = 'rotate(' + rotation + 'deg)';
        element.style.transform       = 'rotate(' + rotation + 'deg)';
    }

    presenter.initializeTexts = function () {
        var up = presenter.state.elements.upButton.getElementsByClassName('Maze-wrapper-menu-controls-no-select')[0];
        var left = presenter.state.elements.leftButton.getElementsByClassName('Maze-wrapper-menu-controls-no-select')[0];
        var right = presenter.state.elements.rightButton.getElementsByClassName('Maze-wrapper-menu-controls-no-select')[0];
        var down = presenter.state.elements.downButton.getElementsByClassName('Maze-wrapper-menu-controls-no-select')[0];

        up.innerText = presenter.configuration.translations.upButton.translation;
        left.innerText = presenter.configuration.translations.leftButton.translation;
        down.innerText = presenter.configuration.translations.downButton.translation;
        right.innerText = presenter.configuration.translations.rightButton.translation;

        presenter.state.elements.lettersAnswerButton.innerText = presenter.configuration.translations.nextMazeButton.translation;
        presenter.state.elements.applyButtonText.innerText = presenter.configuration.translations.applyQuestion.translation;
        presenter.state.elements.endGameButtonText.innerText = presenter.configuration.translations.endGame.translation;
        presenter.state.elements.lettersEndGameText.innerText = presenter.configuration.translations.endGame.translation;
        presenter.state.elements.looseText.innerText = presenter.configuration.translations.loose.translation;
        presenter.state.elements.looseRetryButton.innerText = presenter.configuration.translations.looseButton.translation;
    };

    /**
     * @param  {HTMLDivElement} view
     * @param  {Object} model
     * @param  {Boolean} isPreview
     */
    presenter.runLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.view = view;
        var validatedModel = presenter.validateModel(model);

        if (!validatedModel.isValid) {
            presenter.configuration.isValid = false;
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[validatedModel.fieldName.join("_") + "_" + validatedModel.errorCode]);
            return;
        }

        presenter.configuration = validatedModel.value;
        presenter.configuration.isValid = true;

        presenter.setVisibility(presenter.configuration['Is Visible']);

        if (!isPreview) {
            presenter.completeState();
            presenter.initializeTexts();

            if (presenter.configuration.hideControlPanel) {
                presenter.state.elements.menu.style.display = 'none';
                presenter.state.elements.gameContainer.style.width = '100%';
            }
            presenter.connectHandlers();
            presenter.initializeMaze();

            MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }

        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.onKeyDown = function (event) {
        function stopEvent () {
            event.preventDefault();
            event.stopPropagation();
        }
        switch (event.keyCode) {
            case 37:    //Left
                presenter.moveLeft();
                stopEvent();
                break;
            case 38:    //Up
                presenter.moveUp();
                stopEvent();
                break;
            case 39:    //Right
                presenter.moveRight();
                stopEvent();
                break;
            case 40:    //Down
                presenter.moveDown();
                stopEvent();
                break;
            case 13:    //Enter
                presenter.onQuestionApplyButtonClick();
                presenter.onNextMazeButtonClick();
                stopEvent();
                break;
        }
    };

    presenter.onRetryButtonClick = function () {
        presenter.hideRetryView();
        presenter.sendEvent(presenter.getRetryButtonClickedEventData());
    };

    presenter.initializeMaze = function () {
            var gameContainer = presenter.state.elements.gameContainer,
                i,
                minSize = Math.min(gameContainer.offsetWidth, gameContainer.offsetHeight),
                game;


            if (presenter.configuration.hideControlPanel) {
                minSize = Math.min(presenter.configuration.Width, presenter.configuration.Height);
            }

            presenter.state.actualGameIndex = 0;
            presenter.state.games = [];
            var labirynthSize = {
                width: presenter.configuration.mazeWidth,
                height: presenter.configuration.mazeHeight
            };


            for (i = 0; i &lt; presenter.configuration.numberOfMazes; i += 1) {
                if (presenter.configuration.gameType === presenter.GAME_TYPES.DOORS) {
                    game = new DoorsGame(labirynthSize, minSize, presenter.configuration.questions[i] || []);
                    presenter.state.games.push(game);
                } else  {
                    game = new LetterGame(labirynthSize, minSize, presenter.configuration.questions[i] || []);
                    presenter.state.games.push(game);
                }
            }

            presenter.getActualGame().start(gameContainer);
    };

    /**@returns {Game}
     */
    presenter.getActualGame = function () {
        return presenter.state.games[presenter.state.actualGameIndex];
    };

    function canMove() {
        if (presenter.state.isDisabled || presenter.state._isDisabled) {
            return false;
        }

        if (presenter.state.isShowingErrors || presenter.state.isShowingAnswers) {
            return false;
        }

        return true;
    }

    presenter.moveUp = function () {
        if (canMove()) {
            presenter.getActualGame().goUp();
        }
    };

    presenter.moveDown = function () {
        if (canMove()) {
            presenter.getActualGame().goDown();
        }
    };

    presenter.moveLeft = function () {
        if (canMove()) {
            presenter.getActualGame().goLeft();
        }
    };

    presenter.moveRight = function () {
        if (canMove()) {
            presenter.getActualGame().goRight();
        }
    };

    presenter.onQuestionApplyButtonClick = function () {
        presenter.state.applyButtonClickCallback(presenter.state.elements.answerInput.value);
        presenter.state.elements.answerInput.value = '';
        presenter.state.applyButtonClickCallback = function () {};
    };

    presenter.setOnQuestionApplyCallback = function (fn) {
        presenter.state.applyButtonClickCallback = fn;
    };

    presenter.setQuestionHTML = function (html) {
        presenter.state.elements.questionText.innerHTML = html;
    };

    presenter.showQuestionModal = function () {
        presenter.state._isDisabled = true;
        presenter.state.elements.questionContainer.style.display = 'block';
        presenter.state.elements.questionBackground.style.display = 'block';
        presenter.state.elements.answerInput.focus();
    };

    presenter.hideQuestionModal = function () {
        presenter.state._isDisabled = false;
        presenter.state.elements.questionContainer.style.display = 'none';
        presenter.state.elements.questionBackground.style.display = 'none';
        presenter.state.elements.answerInput.value = '';
        presenter.state.applyButtonClickCallback = function () {};
        presenter.state.elements.addonWrapper.focus();
    };

    presenter.sendEvent = function (evData) {
        if (presenter.eventBus != undefined) {
            presenter.eventBus.sendEvent('ValueChanged', evData);
        }
    };

    presenter.disable = function () {
        presenter.state.isDisabled = true;
    };

    presenter.enable = function () {
        presenter.state.isDisabled = false;
    };

    presenter.getMistakeEventData = function () {
        return {
            source : presenter.configuration.ID,
            value: presenter.state.mistakes + '',
            item: 'mistake'
        };
    };

    presenter.getOpenedDoorEventData = function (number) {
        return {
            source : presenter.configuration.ID,
            value: number + '',
            item: 'opened'
        };
    };

    presenter.openedDoor = function (number) {
        presenter.sendEvent(presenter.getOpenedDoorEventData(number));
    };

    presenter.getGatheredLetterEventData = function (letter) {
        return {
            source : presenter.configuration.ID,
            value: letter,
            item: 'gathered'
        };
    };

    presenter.receivedLetter = function (letter) {
        presenter.sendEvent(presenter.getGatheredLetterEventData(letter));
    };

    presenter.getFinishedMazeEventData = function (mazeNumber) {
        return {
            source: presenter.configuration.ID,
            value: '1',
            item: mazeNumber + '',
            score: 1
        };
    };

    presenter.getFinishedAllMazeEventData = function () {
        return {
            source: presenter.configuration.ID,
            value: '1',
            item: 'all',
            score: 1
        };
    };

    presenter.getRetryButtonClickedEventData = function () {
        return {
            source: presenter.configuration.ID,
            value: '1',
            item: 'retry',
            score: 0
        };
    };

    presenter.showEndGame = function () {
        presenter.state.elements.endGame.style.display = 'block';
        presenter.state._isDisabled = true;
    };

    presenter.hideEndGame = function () {
        presenter.state.elements.endGame.style.display = 'none';
        presenter.state._isDisabled = false;
    };

    presenter.finishedMaze = function () {
        if (presenter.state.actualGameIndex + 1 === presenter.configuration.numberOfMazes) {
            presenter.state.actualGameIndex += 1;
            presenter.showEndGame();
            presenter.sendEvent(presenter.getFinishedAllMazeEventData());
        } else {
            presenter.getActualGame().destroy();
            presenter.state.actualGameIndex += 1;
            presenter.getActualGame().start(presenter.state.elements.gameContainer);
            presenter.sendEvent(presenter.getFinishedMazeEventData(presenter.state.actualGameIndex));
        }
    };

    presenter.setNextMazeButtonCallback = function (fn) {
        presenter.state.nextMazeButtonCallback = fn;
    };

    presenter.showLettersAnswer = function (letters) {
        if (presenter.state.games[presenter.state.actualGameIndex + 1]) {
            presenter.state.elements.lettersAnswerButton.style.display = 'block';
            presenter.state.elements.lettersEndGameText.style.display = 'none';
        } else {
            presenter.state.elements.lettersAnswerButton.style.display = 'none';
            presenter.state.elements.lettersEndGameText.style.display = 'block';
            presenter.sendEvent(presenter.getFinishedAllMazeEventData());
        }

        presenter.state.elements.lettersAnswerBackground.style.display = 'block';
        presenter.state.elements.lettersAnswerContainer.style.display = 'block';

        presenter.state.elements.lettersContainer.innerHTML = '';

        var textContainer = document.createElement('div');
        textContainer.classList.add('Maze_levelSolutionText');
        textContainer.innerText = presenter.configuration.translations.solutionText.translation;
        presenter.state.elements.lettersContainer.appendChild(textContainer);

        letters.forEach(function (element) {
           var div = document.createElement('div');
           div.classList.add('Maze_letters_letter_element');
           div.innerHTML = escape(element).split(' ').join('&amp;nbsp;');
           presenter.state.elements.lettersContainer.appendChild(div);
        });

        presenter.state._isDisabled = true;
    };

    presenter.hideLettersAnswer = function () {
        presenter.state.elements.lettersAnswerBackground.style.display = 'none';
        presenter.state.elements.lettersAnswerContainer.style.display = 'none';
        presenter.state._isDisabled = false;
    };

    presenter.showRetryView = function () {
        presenter.state.elements.looseWrapper.style.display = 'block';
    };

    presenter.hideRetryView = function () {
        presenter.state.elements.looseWrapper.style.display = 'none';
    };

    presenter.playerMistake = function () {
        presenter.state.mistakes += 1;
        presenter.state.errorCount += 1;

        presenter.sendEvent(presenter.getMistakeEventData());

        if (presenter.state.mistakes === 3) {
            presenter.getActualGame().destroy();
            presenter.initializeMaze();
            presenter.state.mistakes = 0;
            presenter.showRetryView();
        }
    };

    presenter.onNextMazeButtonClick = function () {
        presenter.state.nextMazeButtonCallback.call(presenter.getActualGame());
        presenter.setNextMazeButtonCallback(function (){});
    };

    presenter.connectHandlers = function () {
        presenter.state.elements.upButton.addEventListener('click', presenter.moveUp);
        presenter.state.elements.leftButton.addEventListener('click', presenter.moveLeft);
        presenter.state.elements.downButton.addEventListener('click', presenter.moveDown);
        presenter.state.elements.rightButton.addEventListener('click', presenter.moveRight);
        presenter.state.elements.applyButton.addEventListener('click', presenter.onQuestionApplyButtonClick);
        presenter.state.elements.lettersAnswerButton.addEventListener('click', presenter.onNextMazeButtonClick);
        presenter.state.elements.looseRetryButton.addEventListener('click', presenter.onRetryButtonClick);
        presenter.state.elements.addonWrapper.addEventListener('keydown', presenter.onKeyDown);
    };

    presenter.disconnectHandlers = function () {
        presenter.state.elements.upButton.removeEventListener('click', presenter.moveUp);
        presenter.state.elements.leftButton.removeEventListener('click', presenter.moveLeft);
        presenter.state.elements.downButton.removeEventListener('click', presenter.moveDown);
        presenter.state.elements.rightButton.removeEventListener('click', presenter.moveRight);
        presenter.state.elements.applyButton.removeEventListener('click', presenter.onQuestionApplyButtonClick);
        presenter.state.elements.lettersAnswerButton.removeEventListener('click', presenter.onNextMazeButtonClick);
        presenter.state.elements.looseRetryButton.removeEventListener('click', presenter.onRetryButtonClick);
        presenter.state.elements.addonWrapper.removeEventListener('keydown', presenter.onKeyDown);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'moveUp': presenter.moveUp,
            'moveDown': presenter.moveDown,
            'moveLeft': presenter.moveLeft,
            'moveRight': presenter.moveRight,
            'enable': presenter.enable,
            'disable': presenter.disable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.showAnswers = function () {
        presenter.state.isShowingAnswers = true;

        if (presenter.state.showingErrors) {
            presenter.setWorkMode();
        }
    };

    presenter.hideAnswers = function () {
        presenter.state.isShowingAnswers = false;
    };

    presenter.setShowErrorsMode = function() {
        presenter.state.isShowingErrors = true;

        if (presenter.state.isShowingAnswers) {
            presenter.hideAnswers();
        }
    };

    presenter.setWorkMode = function () {
        presenter.state.isShowingErrors = false;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName === 'ShowAnswers') {
            presenter.showAnswers();
        }

        if (eventName === 'HideAnswers') {
            presenter.hideAnswers();
        }
    };

    /**
     *
     * @param {{question:String, answer:String, letter:String, mazeId:Number}[]} questions
     */
    presenter.completeQuestions = function (questions) {
        var questionsObject = [];

        questions.forEach(function (element) {
           if (element.mazeId !== -1) {
               questionsObject[element.mazeId - 1] = questionsObject[element.mazeId - 1] || [];

               questionsObject[element.mazeId -1].push(element);
           }
        });

        return questionsObject;
    };

    presenter.validateModel = function (model) {
        function shouldValidateLetter(localValidated) {
            return localValidated['question'] !== '' &amp;&amp; this.validatedModel['gameMode'] === 'letters';
        }

        if (model.gameMode === '') {
            model.gameMode = 'doors';
        }

        var modelValidator = new ModelValidator();
        var validatedModel = modelValidator.validate(model, [
            ModelValidators.Boolean('Is Visible'),
            ModelValidators.DumbString('ID'),
            ModelValidators.Boolean('hideControlPanel'),
            ModelValidators.Boolean('isDisabled'),
            ModelValidators.Enum('gameMode', {values: ['doors', 'letters'], useLowerCase: true}),
            ModelValidators.Integer('mazeWidth', {minValue: 6}),
            ModelValidators.Integer('mazeHeight', {minValue: 6}),
            ModelValidators.Integer('Width'),
            ModelValidators.Integer('Height'),
            ModelValidators.Integer('numberOfMazes', {minValue: 1, default: 1}),
            ModelValidators.List('questions', [
                ModelValidators.DumbString("question"),
                ModelValidators.DumbString("answer"),
                ModelValidators.String("letter", {trim: false}, shouldValidateLetter),
                ModelValidators.Integer('mazeId', {minValue: 1, default: -1}),
                ModelValidators.Boolean('isCaseSensitive'),
            ]),
            ModelValidators.StaticList('translations', {
                'endGame': [ModelValidators.String('translation', {default: 'End Game'})],
                'applyQuestion': [ModelValidators.String('translation', {default: 'Apply'})],
                'nextMazeButton': [ModelValidators.String('translation', {default: 'Next maze'})],
                'solutionText': [ModelValidators.String('translation', {default: 'Level solution is: '})],
                'upButton': [ModelValidators.String('translation', {default: 'Up'})],
                'leftButton': [ModelValidators.String('translation', {default: 'Left'})],
                'rightButton': [ModelValidators.String('translation', {default: 'Right'})],
                'downButton': [ModelValidators.String('translation', {default: 'Down'})],
                'loose': [ModelValidators.String('translation', {default: 'You loose'})],
                'looseButton': [ModelValidators.String('translation', {default: 'Retry'})]
            })
        ]);

        if (!validatedModel.isValid) {
            return validatedModel;
        }

        validatedModel.value.questions = presenter.completeQuestions(validatedModel.value.questions);
        validatedModel.value.isVisible = validatedModel.value['Is Visible'];
        validatedModel.value.gameType = presenter.GAME_TYPES[validatedModel.value.gameMode.toUpperCase()];

        return validatedModel;
    };

    presenter.getState = function () {
        return JSON.stringify({
            isDisabled: presenter.state.isDisabled,
            actualGameIndex: presenter.state.actualGameIndex,
            mistakes: presenter.state.mistakes,
            errorCount: presenter.state.errorCount,
            actualGame: presenter.getActualGame()? presenter.getActualGame().serialize(): null,
            isVisible: presenter.state.isVisible
        });
    };

    presenter.setState = function (state) {
        var object = JSON.parse(state);
        if (object.actualGameIndex !== 0) {
            presenter.getActualGame().destroy();
        }

        presenter.state.isDisabled = object.isDisabled;
        presenter.state.actualGameIndex = object.actualGameIndex;
        presenter.state.mistakes = object.mistakes;
        presenter.state.errorCount = object.errorCount;

        presenter.setVisibility(object.isVisible);

        if (presenter.getActualGame()) {
            if (object.actualGameIndex !== 0) {
                presenter.getActualGame().start(presenter.state.elements.gameContainer);
            }
            presenter.getActualGame().deserialize(object.actualGame);
        } else {
            presenter.showEndGame();
        }

    };

    presenter.reset = function () {
        presenter.setNextMazeButtonCallback(function () {});
        presenter.state.mistakes = 0;
        presenter.state.isDisabled = presenter.configuration.isDisabled;
        presenter.state.isShowingAnswers = false;
        presenter.state.isShowingErrors = false;
        presenter.setVisibility(presenter.configuration['Is Visible']);

        if (presenter.getActualGame()) {
            presenter.getActualGame().destroy();
        } else {    //If getActualGame returns undefined then player finished all mazes
            presenter.state.actualGameIndex--;
            presenter.getActualGame().destroy();
        }

        presenter.state.games = [];

        presenter.hideQuestionModal();
        presenter.hideEndGame();
        presenter.hideLettersAnswer();
        presenter.hideRetryView();

        presenter.initializeMaze();
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    /**
     * @class
     *
     * @param {{width: Number, height: Number}} size
     * @param {Number} maxSize of maze in px
     *
     */
    function Game (size, maxSize) {
        this.maze = new Maze(size, maxSize);
        this.playerElement = document.createElement('div');
        this.playerElement.classList.add('Maze_player_element');

        this.playerPosition = {
            x: 0,
            y: 0
        };
    }

    Game.prototype.serialize = function () {
        return {
            maze: this.maze.serialize(),
            playerPosition: this.playerPosition
        }
    };

    Game.prototype.deserialize = function (obj) {
        this.playerPosition = obj.playerPosition;
        this.maze.deserialize(obj.maze);
    };

    Game.prototype.getScore = function () {
        return 0;
    };

    /**
     * @param  {HTMLDivElement} container
     */
    Game.prototype.start = function (container) {
        this.maze.generate();
        container.appendChild(this.maze.getElement());
    };

    Game.prototype.destroy = function () {
        if (this.maze.getElement().parentNode) {
            this.maze.getElement().parentNode.removeChild(this.maze.getElement());
        }
        this.maze.mazeElements = [];
        this.maze.rooms = [];
        this.maze.walls = [];
    };

    /**
     * @param {Room}room
     */
    Game.prototype.movePlayerTo = function (room) {
        var checkPromise = this.checkRoomCallback(room);

        if (checkPromise) {
            var self = this;
            checkPromise.then(function (isOpened) {
                if (isOpened) {
                    room.element.appendChild(self.playerElement);
                    self.playerPosition = self.maze.getRoomPosition(room);
                }
            });
        } else {
            room.element.appendChild(this.playerElement);
            this.playerPosition = this.maze.getRoomPosition(room);
        }
    };

    Game.prototype.goUp = function () {
        var wall = this.maze.mazeElements[this.playerPosition.y - 1][this.playerPosition.x];

        if (wall.isOpened()) {
            var room = this.maze.mazeElements[this.playerPosition.y - 2][this.playerPosition.x];
            this.movePlayerTo(room);
        }
    };

    Game.prototype.goLeft = function () {
        var wall = this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x - 1];

        if (wall.isOpened()) {
            var room = this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x - 2];
            this.movePlayerTo(room);
        }
    };

    Game.prototype.goRight = function () {
        var wall = this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x + 1];

        if (wall.isOpened()) {
            var room = this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x + 2];
            this.movePlayerTo(room);
        }
    };

    Game.prototype.goDown = function () {
        var wall = this.maze.mazeElements[this.playerPosition.y + 1][this.playerPosition.x];

        if (wall.isOpened()) {
            var room = this.maze.mazeElements[this.playerPosition.y + 2][this.playerPosition.x];
            this.movePlayerTo(room);
        }
    };

    /**
     * Call room callback if exists
     * @param {Room} room
     * @returns {Object|null} Promise if callback exists or null
     */
    Game.prototype.checkRoomCallback = function (room) {
        if (room.callback) {
            return room.callback.call(this, room);
        }

        return null;
    };

    function LetterGame (size, maxSize, questions) {
        Game.call(this, size, maxSize);

        this.questionsToAnswer = 0;
        this.questions = questions;

        this.isShowingEndGame = false;
        this.endLevelElement = document.createElement('div');
        this.endLevelElement.classList.add('Maze_letters_end_level');
        this.gatheredLettersCount = 0;
        this.questionsPositions = [];   //Saving question as [index] = roomIndex, if roomIndex is null then question is resolved
    }

    LetterGame.prototype = Object.create(Game.prototype);

    LetterGame.prototype.getScore = function () {
        return this.isShowingEndGame? 1 : 0;
    };

    LetterGame.prototype.serialize = function () {
        var gameObj = Game.prototype.serialize.call(this);

        return {
            gameObj: gameObj, 
            gatheredLettersCount: this.gatheredLettersCount,
            questionsPositions: this.questionsPositions,
            isShowingEndGame: this.isShowingEndGame
        };
    };

    LetterGame.prototype.deserialize = function (obj) {
        Game.prototype.deserialize.call(this, obj.gameObj);
        
        this.gatheredLettersCount = obj.gatheredLettersCount;
        var longestPath = this.maze.getLongestPath();

        this.createEndLevelElement(longestPath);
        this.questionsPositions = obj.questionsPositions;

        var self = this;
        this.questionsPositions.forEach(function (element, index) {
            if (element !== null) {
                self.setLetterInRoom(self.maze.rooms[element], self.questions[index]);
            }
        });

        this.movePlayerTo(this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x]);

        if (obj.isShowingEndGame) {
            this.onEnterEndGame();
        }
    };

    LetterGame.prototype.start = function (container) {
        Game.prototype.start.call(this, container);

        var longestPath = this.maze.getLongestPath();
        this.movePlayerTo(longestPath[0]);
        this.createEndLevelElement(longestPath);

        var questions = this.questions.slice();

        while(questions.length !== 0) {
            var questionIndex = getRandomIndex(questions);

            var question = questions.splice(questionIndex, 1)[0];
            var counter = 0;

            while (true) {
                counter++;

                var roomIndex = getRandomIndex(this.maze.rooms);
                var room = this.maze.rooms[roomIndex];

                if (room === longestPath[0]) {
                    continue;
                }

                if (!room.hasCallback()) {
                    this.setLetterInRoom(room, question);
                    this.questionsPositions[this.questions.indexOf(question)] = roomIndex;
                    this.questionsToAnswer++;
                    break;
                }

                //Be sure that there never will be forever while
                if (counter &gt;= 50) {
                    return;
                }
            }
        }
    };

    /**
     *
     * @param room {Room}
     * @param question {{}}
     */
    LetterGame.prototype.setLetterInRoom = function (room, question) {
        room.setCallback(this.onLetterEnterCallback.bind(this, question));

        var paragraph = document.createElement('p');
        paragraph.classList.add('Maze_letters_room_letter');
        paragraph.innerText = question.letter;

        room.getElement().appendChild(paragraph);        
    };

    LetterGame.prototype.onLetterEnterCallback = function (questionObj, room) {
        var promise = $.Deferred();
        presenter.setQuestionHTML(questionObj.question);
        presenter.showQuestionModal();


        var self = this;
        presenter.setOnQuestionApplyCallback(function (value) {
            var answer = questionObj.answer;
            if (!questionObj.isCaseSensitive) {
                value = value.toLowerCase();
                answer = answer.toLowerCase();
            }

            if (value === answer) {
                self.gatheredLettersCount += 1;
                room.removeCallback();
                var letter = room.element.getElementsByClassName('Maze_letters_room_letter')[0];
                letter.parentNode.removeChild(letter);

                presenter.receivedLetter(questionObj.letter);
                self.questionsPositions[self.questions.indexOf(questionObj)] = null;
                promise.resolve(true);
            } else {
                presenter.playerMistake();
                promise.resolve(false);
            }

            presenter.hideQuestionModal();
        });

        return promise;
    };

    LetterGame.prototype.createEndLevelElement = function (longestPath) {
        longestPath[longestPath.length - 1].element.appendChild(this.endLevelElement);
        longestPath[longestPath.length - 1].setCallback(this.onEnterEndGame);
    };

    LetterGame.prototype.onEnterEndGame = function () {
        var i,
            letters = [],
            promise = $.Deferred();

        if (this.questionsToAnswer !== this.gatheredLettersCount) {
            promise.resolve(false);
            return promise;
        }

        this.isShowingEndGame = true;

        for (i = 0; i &lt; this.questions.length; i += 1) {
            letters.push(this.questions[i].letter);
        }

        presenter.showLettersAnswer(letters);
        presenter.setNextMazeButtonCallback(function () {
            if (presenter.state.games[presenter.state.actualGameIndex + 1]) {
                presenter.finishedMaze();
                presenter.hideLettersAnswer();
            }
            promise.resolve(true);
        });

        return promise;
    };

    /**
     *
     * @param size
     * @param maxSize
     * @param {{}[]}questions
     * @constructor
     */
    function DoorsGame (size, maxSize, questions) {
        Game.call(this, size, maxSize);

        this.treasureElement = document.createElement('div');
        this.treasureElement.classList.add('Maze_treasure_element');
        this.questions = questions;
        this.keysCount = 0;
    }

    DoorsGame.prototype = Object.create(Game.prototype);

    DoorsGame.prototype.serialize = function () {
        var gameObj = Game.prototype.serialize.call(this);

        return {
            gameObj: gameObj, 
            keysCount: this.keysCount
        };
    };

    DoorsGame.prototype.deserialize = function (obj) {
        Game.prototype.deserialize.call(this, obj.gameObj);

        this.keysCount = obj.keysCount;
        var longestPath = this.maze.getLongestPath(),
            doorsCount = this.questions.length,
            spaceBetween = Math.max(1, Math.floor(longestPath.length / (doorsCount + 1)));
        
        this.createTreasureElement(longestPath);
        this.createDoors(longestPath);

        for (var i = 0; i &lt; this.keysCount; i++) {
            this.openDoor(longestPath[(i + 1) * spaceBetween]);
        }

        this.movePlayerTo(this.maze.mazeElements[this.playerPosition.y][this.playerPosition.x]);        
    };

    DoorsGame.prototype.start = function (container) {
        Game.prototype.start.call(this, container);
        var longestPath = this.maze.getLongestPath();
        this.movePlayerTo(longestPath[0]);

        this.createTreasureElement(longestPath);
        this.createDoors(longestPath);
    };

    /**
     *val
     * @param {Room[]} longestPath
     */
    DoorsGame.prototype.createDoors = function (longestPath) {
        var doorsCount = this.questions.length,
            spaceBetween = Math.max(1, Math.floor(longestPath.length / (doorsCount + 1))),
            i;

        for(i = 1; i &lt;= Math.min(doorsCount, longestPath.length - 2); i += 1) {
            this.createDoorElement(longestPath[i * spaceBetween]);
        }
    };

    DoorsGame.prototype.createDoorElement = function (room) {
        var element = document.createElement('div');
        element.classList.add('Maze_door');

        room.element.appendChild(element);
        room.setCallback(this.onEnterDoor);
    };

    /**
     *
     * @param {Room} room
     */
    DoorsGame.prototype.createOpenedDoorElement = function (room) {
        var element = document.createElement('div');
        element.classList.add('Maze_door_opened');

        room.element.appendChild(element);
    };

    /**
     *
     * @param {Room} room
     * @returns {boolean}
     */
    DoorsGame.prototype.onEnterDoor = function (room) {
        var promise = $.Deferred();

        presenter.setQuestionHTML(this.questions[this.keysCount].question);
        presenter.showQuestionModal();

        var self = this;
        presenter.setOnQuestionApplyCallback(function (value) {
            var answer = self.questions[self.keysCount].answer;
            if (!self.questions[self.keysCount].isCaseSensitive) {
                value = value.toLowerCase();
                answer = answer.toLowerCase();
            }

            if (value === answer) {
                self.openDoor(room);
                self.keysCount += 1;

                presenter.openedDoor(self.keysCount);
                promise.resolve(true);
            } else {
                presenter.playerMistake();
                promise.resolve(false);
            }

            presenter.hideQuestionModal();
        });

        return promise;
    };

    DoorsGame.prototype.openDoor = function (room) {
        var door = room.element.getElementsByClassName('Maze_door')[0];
        door.parentNode.removeChild(door);
        this.createOpenedDoorElement(room);
        room.removeCallback();
    };

    DoorsGame.prototype.createTreasureElement = function (longestPath) {
        longestPath[longestPath.length - 1].element.appendChild(this.treasureElement);
        longestPath[longestPath.length - 1].setCallback(this.onEnterTreasure);
    };

    DoorsGame.prototype.onEnterTreasure = function () {
        var promise = $.Deferred();

        presenter.finishedMaze(promise);
        promise.resolve(true);

        return promise;
    };

    /**
     * @class
     * @param {Number} index
     *
     * @this Wall
     */
    function Wall (index) {
        this.index = index;

        this.state = this.STATES.close;
    }

    Wall.prototype.serialize = function () {
        return {
            state: this.state
        };
    };

    Wall.prototype.deserialize = function (obj) {
        this.state = obj.state;
    };

    /**
     * close this wall
     */
    Wall.prototype.close = function () {
        this.state = this.STATES.CLOSE;
    };

    Wall.prototype.open = function () {
        this.state = this.STATES.OPEN;
    };

    Wall.prototype.STATES = {
        OPEN: 1,
        CLOSE: 2
    };

    Wall.prototype.isClosed = function () {
        return this.state === this.STATES.CLOSE;
    };

    Wall.prototype.isOpened = function () {
        return this.state === this.STATES.OPEN;
    };

    /**
     * @class
     * 
     * @param {Number} index
     */
    function Room(index) {
        this.index = index;

        this.element = document.createElement('div');
        this.element.classList.add('Maze_Room_container');
        this.walls = document.createElement('div');
        this.element.appendChild(this.walls);
        this.element.classList.add('Maze_room');
        this.callback = null;
    }

    Room.prototype.serialize = function () {
        return {
        };
    };

    Room.prototype.deserialize = function (obj) {
    };

    Room.prototype.hasCallback = function () {
        return this.callback;
    };

    Room.prototype.getElement = function() {
        return this.element;
    };

    Room.prototype.addWallClass = function (className) {
        this.walls.classList.add(className);
    };

    Room.prototype.getWallsElement = function () {
        return this.walls;
    };

    Room.prototype.setCallback = function (callback) {
        this.callback = callback;
    };

    Room.prototype.removeCallback = function () {
        this.callback = null;
    };

    Room.prototype.setDotElement = function (rotation) {
        var dotDiv = document.createElement('div');
        dotDiv.classList.add('Maze_room_left_top_dot');

        rotateElement(dotDiv, rotation);
        this.getElement().appendChild(dotDiv);
    };

    /**
     * Helper for storing maze in memory as square
     */
    function Edge () {

    }

    Edge.prototype.serialize = function () {
        return {

        };
    };

    Edge.prototype.deserialize = function (obj) {

    };

    /**
     * @class
     *
     * @param {{width: Number, height: Number}} size
     * @param {Number} maxSize of maze in px
     * @this Maze
     *
     */
    function Maze (size, maxSize) {
        this.xSize = size.width;
        this.ySize = size.height;
        this.maxSize = maxSize;

        this.mazeElements = []; //All elements in maze. This is {Edge|Wall|Room}[y][x]

        this.mainDiv = document.createElement('div');
        this.mainDiv.className += ' Maze_main_container';

        /** @type {Wall[]} */
        this.walls = [];
        /**@type {Room[]} */
        this.rooms = [];
    }

    Maze.prototype.serialize = function () {
        var elements = [];

        this.mazeElements.forEach(function (row) {
            var rowElements = [];
            row.forEach(function(value) {
                rowElements.push(value.serialize());
            });

            elements.push(rowElements);
        });

        return {
            elements: elements
        };
    };

    Maze.prototype.deserialize = function (mazeObj) {
        this.getMazeElementsContainer().innerHTML = '';
        this.walls = [];
        this.rooms = [];
        this.mazeElements = [];
        this.buildStruct();

        var self = this;
        mazeObj.elements.forEach(function (rowObj, yIndex) {
            rowObj.forEach(function (value, xIndex) {
                self.mazeElements[yIndex][xIndex].deserialize(value);
            });
        });

        this.setValidClasses();
    };

    /**
     * Build structure of maze in memory
     */
    Maze.prototype.buildStruct = function () {
        var i;

        for (i = 0; i &lt; this.ySize; i++) {
            this.buildWallsLine();
            this.buildRoomsLine();
        }

        this.buildWallsLine();
    };

    Maze.prototype.buildWallsLine = function () {
            var i,
                elements = [];

            for (i = 0; i &lt; this.xSize; i++) {
                elements.push(new Edge());
                elements.push(this.buildWall());
            }

            elements.push(new Edge());

            this.mazeElements.push(elements);
    };

    Maze.prototype.buildRoomsLine = function () {
            var i,
                elements = [];

            for (i = 0; i &lt; this.xSize; i++) {
                elements.push(this.buildWall());
                elements.push(this.buildRoom());
            }

            elements.push(this.buildWall());

            this.mazeElements.push(elements);
    };

    Maze.prototype.buildWall = function () {
            var wall = new Wall(this.walls.length);

            this.walls.push(wall);

            return wall;
    };

    Maze.prototype.buildRoom = function () {
            var room = new Room(this.rooms.length);

            this.rooms.push(room);
            this.getMazeElementsContainer().appendChild(room.getElement());

            return room;
    };

    Maze.prototype.algorithms = {
        PRIMS: Prims
    };

    Maze.prototype.getMazeElementsContainer = function () {
        return this.mazeElementsContainer;
    };

    /**
     * Generate new maze
     * @param {"PRIMS"} [algorithmName] name of algorithm which will generate maze
     */
    Maze.prototype.generate = function (algorithmName) {
        algorithmName = algorithmName || 'PRIMS';

        this.mazeElementsContainer = document.createElement('div');
        this.mazeElementsContainer.classList.add('Maze_game_elements_container');

        this.buildStruct();

        this.getElement().appendChild(this.mazeElementsContainer);

        //Call algorithm to build maze
        this.algorithms[algorithmName](this);

        this.setValidClasses();

    };

    /**
    * @returns {Number} count of walls in maze.
    */
    Maze.prototype.getWallsCount = function () {
        return this.walls.length;
    };

    /**
     * @returns {HTMLDivElement}
     */
    Maze.prototype.getElement = function () {
        return this.mainDiv;
    };

    /**
     * Set classes and styles for maze after generating valid maze.
     * Each maze cell is image with correct class name and rotation.
     * Sometimes cell contains single dot in corner to complete this corner.
     */
    Maze.prototype.setValidClasses = function () {
        var i;

        for (i = 0; i &lt; this.rooms.length; i++) {
            var room = this.rooms[i];

            var percent = (100 / Math.max(this.xSize, this.ySize));
            room.getElement().style.width = Math.floor(this.maxSize * percent / 100) + 'px';
            room.getElement().style.height = Math.floor(this.maxSize * percent / 100) + 'px';

            if (i % this.xSize === 0) {
                room.getElement().style.clear = 'both';
            }

            var roomWalls = this.getRoomWalls(room);
            var matchedClass = this.getCorrectClass(roomWalls);

            room.addWallClass(matchedClass.className);

            rotateElement(room.getWallsElement(), matchedClass.rotation * -1);

            this.checkDotIsNeeded(1, 1, 0, room);
            this.checkDotIsNeeded(-1, 1, 90, room);
            this.checkDotIsNeeded(-1, -1, 180, room);
            this.checkDotIsNeeded(1, -1, 270, room);
        }
    };

    /**
     * Sometimes on connection between two walls is needed to put small image for valid display (in corner)
     * @param xSign {Number} check x position with positive sign or negative
     * @param ySign {Number} check y position with positive sign or negative
     * @param rotation {Number}
     * @param room {Room}
     */
    Maze.prototype.checkDotIsNeeded = function (xSign, ySign, rotation, room) {
        var roomPosition = this.getRoomPosition(room);

        if (this.mazeElements[roomPosition.y - (1 * ySign)] &amp;&amp; this.mazeElements[roomPosition.y - (2 * ySign)]) {
                var leftWall = this.mazeElements[roomPosition.y - (1 * ySign)][roomPosition.x - (2 * xSign)];
                var topWall = this.mazeElements[roomPosition.y - (2 * ySign)][roomPosition.x - (1 * xSign)];
                if (leftWall &amp;&amp; topWall &amp;&amp; leftWall.isClosed() &amp;&amp; topWall.isClosed()) {
                    room.setDotElement(rotation);
                }
            }
    };

    /**
     * Rotate received walls and try match it to predefined values. Returns the best matched class name and rotation.
     * Be sure that order is correct.
     * @param  {Wall[]} walls in order top, right, bottom, left
     */
    Maze.prototype.getCorrectClass = function (walls) {
        var quad = [true, true, true, true];
        var leftTopRight = [true, true, false, true];
        var leftTop = [true, false, false, true];
        var topBottom = [true, false, true, false];
        var top = [true, false, false, false];

        var matchers = [top, topBottom, leftTop, leftTopRight, quad];

        function match () {
            var i,
                j;
            for (i = matchers.length - 1; i &gt;= 0 ; i--) {
                var matcher = matchers[i],
                    isMatched = true;
      
                for (j = 0; j &lt; 4; j++) {
                    if (matcher[j] !== walls[j].isClosed()) {
                        isMatched = false;
                        break;
                    }
                }

                if (isMatched) {
                    return i;
                }
            }

            return false;
        }

        var matchedRotation = 0,
            bestMatch = -1,
            rotation;

        for (rotation = 0; rotation &lt; 360; rotation += 90) {
            var matched = match();
            if (matched !== false &amp;&amp; matched &gt; bestMatch) {
                bestMatch = matched;
                matchedRotation = rotation;
            }

            var wall = walls.pop();
            walls.unshift(wall);
        }

        var classes = {
            4: 'Maze_room_image_top_left_right_bottom',
            3: 'Maze_room_image_top_left_right',
            2: 'Maze_room_image_top_left',
            1: 'Maze_room_image_top_bottom',
            0: 'Maze_room_image_top'
        };

        return {
            className: classes[bestMatch],
            rotation: matchedRotation
        };
    };

    /**Returns room walls in order: top, right, bottom, left
     * @returns  {Wall[]}
     */
    Maze.prototype.getRoomWalls = function (room) {
        var roomPosition = this.getRoomPosition(room);

        var top = this.mazeElements[roomPosition.y - 1][roomPosition.x];
        var left = this.mazeElements[roomPosition.y][roomPosition.x - 1];
        var bottom = this.mazeElements[roomPosition.y + 1][roomPosition.x];
        var right = this.mazeElements[roomPosition.y][roomPosition.x + 1];

        return [top, right, bottom, left];
    };

    /**
     * Get room position in maze.
     * @param {Room} room 
     */
    Maze.prototype.getRoomPosition = function (room) {
        var roomXPosition = room.index % this.xSize,
            roomYPosition = ~~(room.index / this.xSize);

        var roomYPositionInElements = (roomYPosition * 2) + 1,
            roomXPositionInElements = (roomXPosition * 2) + 1;

        return {
            y: roomYPositionInElements,
            x: roomXPositionInElements
        };
    };

    /**Will return all neighbors
     * @param  {Room} room
     */
    Maze.prototype.getRoomNeigh = function (room) {
        var roomPosition = this.getRoomPosition(room);

        var roomYPositionInElements = roomPosition.y,
            roomXPositionInElements = roomPosition.x;

        var neigh = [];

        if (roomXPositionInElements - 2 &gt; 0) {
            neigh.push(this.mazeElements[roomYPositionInElements][roomXPositionInElements - 2]);
        }

        if (roomYPositionInElements - 2 &gt; 0) {
            neigh.push(this.mazeElements[roomYPositionInElements - 2][roomXPositionInElements]);
        }    

        if (roomXPositionInElements + 2 &lt; this.mazeElements[0].length) {
            neigh.push(this.mazeElements[roomYPositionInElements][roomXPositionInElements + 2]);
        } 

        if (roomYPositionInElements + 2 &lt; this.mazeElements.length) {
            neigh.push(this.mazeElements[roomYPositionInElements + 2][roomXPositionInElements]);
        } 

        return neigh;
    };

    /**
     * @param  {Room} roomA
     * @param  {Room} roomB
     */
    Maze.prototype.openWallBetween = function (roomA, roomB) {
        var roomAPosition = this.getRoomPosition(roomA),
            roomBPosition = this.getRoomPosition(roomB);

        var wallXPosition = roomAPosition.x - (roomAPosition.x - roomBPosition.x) / 2,
            wallYPosition = roomAPosition.y - (roomAPosition.y- roomBPosition.y) / 2;

        this.mazeElements[wallYPosition][wallXPosition].open();
    };

    Maze.prototype.isOpenedBetween = function (roomA, roomB) {
        var roomAPosition = this.getRoomPosition(roomA),
            roomBPosition = this.getRoomPosition(roomB);

        var wallXPosition = roomAPosition.x - (roomAPosition.x - roomBPosition.x) / 2,
            wallYPosition = roomAPosition.y - (roomAPosition.y- roomBPosition.y) / 2;

        return this.mazeElements[wallYPosition][wallXPosition].isOpened();
    };

    /**
     * Get longest path in maze.
     * Algorithm: get random element and find longest path from it. Get last element from that longest path and find next longest path which is longest path in maze
     * @returns {Array}
     */
    Maze.prototype.getLongestPath = function () {
        var self = this;
        var bestAnswer = [];
        function dfs(room, from, answer) {
            answer.push(room);

            var neigh = self.getRoomNeigh(room).filter(function (element) {
                return self.isOpenedBetween(room, element);
            });

            neigh.forEach(function (element) {
                if (element !== from) {
                    dfs(element, room, answer.slice());
                }
            });

            if (neigh.length === 1) {
                if (answer.length &gt; bestAnswer.length) {
                    bestAnswer = answer;
                }
            }
        }
        var room = this.rooms[0];

        dfs(room, null, []);
        room = bestAnswer[bestAnswer.length - 1];

        bestAnswer = [];
        dfs(room, null, []);

        return bestAnswer;
    };

    function getRandomIndex (array) {
        return Math.floor(Math.random() * array.length);
    }

    /**http://weblog.jamisbuck.org/2011/1/10/maze-generation-prim-s-algorithm
     * @param  {Maze} maze
     */
    function Prims (maze) {
        var roomsList = [];
        var selected = [];

        function initialize () {
            var i;

            for (i = 0; i &lt; maze.getWallsCount(); i++) {
                maze.walls[i].close();
            }

            var neigh = maze.getRoomNeigh(maze.rooms[0]);
            roomsList = roomsList.concat(neigh);
            selected.push(maze.rooms[0]);
        }

        initialize();

        while (roomsList.length !== 0) {
            var roomIndex = getRandomIndex(roomsList);
            var room = roomsList.splice(roomIndex, 1)[0];
            var roomNeigh = maze.getRoomNeigh(room);
            var selectedNeight = [];

            roomNeigh.forEach(function (element) {
                if (selected.indexOf(element) === -1) {
                    if (roomsList.indexOf(element) === -1) {
                        roomsList.push(element);
                    }
                } else {
                    selectedNeight.push(element);
                }
            });

            var connectWithIndex = getRandomIndex(selectedNeight);
            maze.openWallBetween(room, selectedNeight[connectWithIndex]);
            selected.push(room);

        }

    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Media_Recorder" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="isDisabled" nameLabel="Media_Recorder_property_is_disabled" type="boolean"/>
        <property name="maxTime" nameLabel="Media_Recorder_property_max_time" type="integer"/>
        <property name="defaultRecording" nameLabel="Media_Recorder_property_default_recording" type="audio"/>
        <property name="startRecordingSound" nameLabel="Media_Recorder_property_start_recording_sound" type="audio"/>
        <property name="stopRecordingSound" nameLabel="Media_Recorder_property_stop_recording_sound" type="audio"/>
        <property name="isShowedTimer" nameLabel="Media_Recorder_property_is_showed_timer" type="boolean"/>
        <property name="isShowedDefaultRecordingButton" nameLabel="Media_Recorder_property_is_showed_default_recording_button" type="boolean"/>
        <property name="isResetRemovesRecording" nameLabel="Media_Recorder_property_is_reset_removes_recording" type="boolean"/>
        <property name="enableInErrorCheckingMode" nameLabel="Media_Recorder_property_enable_in_error_checking_mode" type="boolean"/>
        <property name="extendedMode" nameLabel="Media_Recorder_property_extended_mode" type="boolean"/>
        <property name="disableRecording" nameLabel="Media_Recorder_property_is_disable_recording" type="boolean"/>
        <property name="enableIntensityChangeEvents" nameLabel="Media_Recorder_property_is_enable_intensity_events" type="boolean"/>
        <property name="resetDialogLabels" nameLabel="Media_Recorder_property_reset_dialog_labels" type="staticlist">
            <property name="resetDialogText" nameLabel="Media_Recorder_property_reset_dialog_text" type="staticrow">
                <property name="resetDialogLabel" nameLabel="Media_Recorder_property_reset_dialog_label" type="string"/>
            </property>
            <property name="resetDialogConfirm" nameLabel="Media_Recorder_property_reset_dialog_confirm" type="staticrow">
                <property name="resetDialogLabel" nameLabel="Media_Recorder_property_reset_dialog_label" type="string"/>
            </property>
            <property name="resetDialogDeny" nameLabel="Media_Recorder_property_reset_dialog_deny" type="staticrow">
                <property name="resetDialogLabel" nameLabel="Media_Recorder_property_reset_dialog_label" type="string"/>
            </property>
        </property>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Media_Recorder_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Media_Recorder_property_speech_texts" type="staticlist">
            <property name="DefaultRecordingPlayButton" nameLabel="Media_Recorder_speech_text_default_recording_play_button" type="staticrow">
                <property name="DefaultRecordingPlayButton" nameLabel="Media_Recorder_speech_text_default_recording_play_button" type="string"/>
            </property>
            <property name="RecordingButton" nameLabel="Media_Recorder_speech_text_recording_button" type="staticrow">
                <property name="RecordingButton" nameLabel="Media_Recorder_speech_text_recording_button" type="string"/>
            </property>
            <property name="PlayButton" nameLabel="Media_Recorder_speech_text_play_button" type="staticrow">
                <property name="PlayButton" nameLabel="Media_Recorder_speech_text_play_button" type="string"/>
            </property>
            <property name="ResetButton" nameLabel="Media_Recorder_speech_text_reset_button" type="staticrow">
                <property name="ResetButton" nameLabel="Media_Recorder_speech_text_reset_button" type="string"/>
            </property>
            <property name="DownloadButton" nameLabel="Media_Recorder_speech_text_download_button" type="staticrow">
                <property name="DownloadButton" nameLabel="Media_Recorder_speech_text_download_button" type="string"/>
            </property>
            <property name="ResetDialog" nameLabel="Media_Recorder_speech_text_reset_dialog" type="staticrow">
                <property name="ResetDialog" nameLabel="Media_Recorder_speech_text_reset_dialog" type="string"/>
            </property>
            <property name="StartRecording" nameLabel="Media_Recorder_speech_text_start_recording" type="staticrow">
                <property name="StartRecording" nameLabel="Media_Recorder_speech_text_start_recording" type="string"/>
            </property>
            <property name="StopRecording" nameLabel="Media_Recorder_speech_text_stop_recording" type="staticrow">
                <property name="StopRecording" nameLabel="Media_Recorder_speech_text_stop_recording" type="string"/>
            </property>
            <property name="Disabled" nameLabel="Media_Recorder_speech_text_disabled" type="staticrow">
                <property name="Disabled" nameLabel="Media_Recorder_speech_text_disabled" type="string"/>
            </property>
        </property>
    </model>
<css>.media-recorder-wrapper {
    position: absolute;
    width: auto;
    height: auto;
    display: block;
}

.media-recorder-wrapper .media-recorder-player-wrapper {
    position: relative;
    width: auto;
    height: auto;
}

.media-recorder-wrapper .media-recorder-interface-wrapper {
    width: 300px;
    height: 40px;
    display: flex;
    align-items: center;
    background-color: #EBEFF0;
    border-radius: 20px;
    padding-left: 10px;
    -webkit-border-radius: 20px;
    -moz-border-radius: 20px;
}

.media-recorder-wrapper .media-recorder-recording-button {
    width: 25px;
    height: 25px;
    margin: 0px 3px;
    background: url('resources/003-record-button.svg') no-repeat center;
    background-size: 100%;
}

.media-recorder-wrapper .media-recorder-recording-button.selected {
    background: black url('resources/004-stop-button.svg') no-repeat center;
    border-radius: 50%;
    background-size: 100%;
}

.media-recorder-wrapper .media-recorder-play-button {
    width: 25px;
    height: 25px;
    margin: 0px 3px;
    background: url('resources/001-play-button.svg') no-repeat center;
    background-size: 100%;
}

.media-recorder-wrapper .media-recorder-play-button.selected {
    background: black url('resources/004-stop-button.svg') no-repeat center;
    background-size: 100%;
    border-radius: 50%;
}

.media-recorder-wrapper .media-recorder-default-recording-play-button {
    width: 25px;
    height: 25px;
    margin: 0px 3px;
    background: url('resources/002-play-default-recording-button.svg') no-repeat center;
    background-size: 100%;
}

.media-recorder-wrapper .media-recorder-default-recording-play-button.selected {
    background: black url('resources/004-stop-button.svg') no-repeat center;
    background-size: 100%;
    border-radius: 50%;
}

.media-recorder-wrapper .media-recorder-timer {
    width: 100px;
    height: 30px;
    line-height: 30px;
    margin: 0px 10px;
    text-align: center;
    font-size: 14px;
    color: grey;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
}

.media-recorder-wrapper.extended-mode .media-recorder-timer {
    width: auto;
}

.media-recorder-wrapper .media-recorder-sound-intensity {
    width: 20px;
    margin: 0px 10px;
    display: flex;
    flex-direction: column;
}

.media-recorder-wrapper .media-recorder-dotted-sound-intensity {
    width: 25px;
    height: 25px;
    margin: 0px 10px;
    display: flex;
    flex-direction: row;
}

.media-recorder-wrapper .media-recorder-dot-container {
    height: 100%;
    position: relative;
    width: 8px;
}

.media-recorder-wrapper .sound-intensity-dot {
    width: 6px;
    height: 6px;
    border-radius: 6px;
    display: block;
    background-color: #1870b7;
    margin: 0;
    position: absolute;
    top: 50%;
    -ms-transform: translateY(-50%);
    transform: translateY(-50%);
}

.media-recorder-wrapper .center {
    margin: 0;
    position: absolute;
    top: 50%;
    -ms-transform: translateY(-50%);
    transform: translateY(-50%);
  }

.media-recorder-wrapper .sound-intensity-large:first-child {
    border-top: 1px solid;
}

.media-recorder-wrapper .sound-intensity-large {
    width: 100%;
    height: 3px;
    border-bottom: 1px solid;
    background-color: #990000;
    margin: auto;
}

.media-recorder-wrapper .sound-intensity-large.selected {
    background-color: #FF0000;
}

.media-recorder-wrapper .sound-intensity-medium {
    width: 100%;
    height: 3px;
    border-bottom: 1px solid;
    background-color: #999900;
    margin: auto;
}

.media-recorder-wrapper .sound-intensity-medium.selected {
    background-color: #E4E400;
}

.media-recorder-wrapper .sound-intensity-low {
    width: 100%;
    height: 3px;
    border-bottom: 1px solid;
    background-color: #4C9900;
    margin: auto;
}

.media-recorder-wrapper .sound-intensity-low.selected {
    background-color: #71E200;
}

.media-recorder-wrapper .media-recorder-player-loader {
}

.media-recorder-wrapper .video-loader {
    border: 5px solid #f3f3f3;
    border-top: 5px solid #1d2830;
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    position: absolute;
    margin: auto;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
}

.media-recorder-wrapper.disabled {
    opacity: 0.5;
}

.media-recorder-wrapper .media-recorder-reset-button {
    width: 25px;
    height: 25px;
    margin: 0px 3px;
    background-size: 100%;
    background: url('resources/005-reset-button.svg') no-repeat center;
    display: none;
}

.media-recorder-wrapper .media-recorder-download-button {
    width: 25px;
    height: 25px;
    margin: 0px 3px;
    background-size: 100%;
    background: url('resources/006-download-button.svg') no-repeat center;
    display: none;
}

.media-recorder-wrapper .media-recorder-progress-bar {
    width: 80px;
    height: 10px;
    margin: 1px 1px;
    display: none;
    background-color: white;
    border-radius: 5px;
    position: relative;
}

.media-recorder-wrapper .media-recorder-progress-bar-slider {
    width: 10px;
    height: 10px;
    display: block;
    background-color: black;
    border-radius: 5px;
    position: absolute;
    cursor: pointer;
}

.media-recorder-wrapper .media-recorder-progress-bar-wrapper {
    width: 80px;
    height: 25px;
    margin: 0px 3px;
    display: none
}

.media-recorder-wrapper .media-recorder-reset-dialog {
    display: none;
    width:300px;
    padding: 10px 10px 15px;
    position:absolute;
    top:-90px;
    background-color: white;
    box-shadow: 8px 8px 5px rgb(0 0 0 / 40%);
    z-index: 99999;
}

.media-recorder-wrapper .media-recorder-reset-dialog .dialog-text {
    text-align: center;
    padding: 5px 5px 10px;
}

.media-recorder-wrapper .media-recorder-reset-dialog .dialog-buttons {
    display: flex;
}

.media-recorder-wrapper .media-recorder-reset-dialog .deny-button {
    margin: auto;
    text-align: center;
    background-color: red;
    color: white;
    padding: 3px 15px;
    display: block;
    cursor: pointer;
}

.media-recorder-wrapper .media-recorder-reset-dialog .confirm-button {
    margin: auto;
    text-align: center;
    background-color: green;
    color: white;
    padding: 3px 15px;
    display: block;
    cursor: pointer;
}

.disable-record-button {
    pointer-events: none;
  	cursor: default;
	opacity: 0.5;
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }
    100% {
        transform: rotate(360deg);
    }
}
</css><view>&lt;div class="media-recorder-wrapper"&gt;
    &lt;div class="media-recorder-player-wrapper"&gt;
        &lt;div class="media-recorder-player-loader"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="media-recorder-interface-wrapper"&gt;
        &lt;div class="media-recorder-default-recording-play-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-recording-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-play-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-timer"&gt;00:00/00:00&lt;/div&gt;
        &lt;div class="media-recorder-sound-intensity"&gt;
            &lt;div class="sound-intensity-large" id="sound-intensity-6"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-large" id="sound-intensity-5"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-medium" id="sound-intensity-4"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-medium" id="sound-intensity-3"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-low" id="sound-intensity-2"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-low" id="sound-intensity-1"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-dotted-sound-intensity"&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot short-dot"&gt;&lt;/div&gt;&lt;/div&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot tall-dot"&gt;&lt;/div&gt;&lt;/div&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot short-dot"&gt;&lt;/div&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-progress-bar"&gt;
            &lt;div class="media-recorder-progress-bar-slider"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-reset-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-download-button"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;div class="media-recorder-reset-dialog"&gt;
            &lt;div class="dialog-text"&gt;&lt;/div&gt;
            &lt;div class="dialog-buttons"&gt;
                &lt;div class="confirm-button"&gt;&lt;/div&gt;
                &lt;div class="deny-button"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="media-recorder-wrapper"&gt;
    &lt;div class="media-recorder-player-wrapper"&gt;
        &lt;div class="media-recorder-player-loader"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="media-recorder-interface-wrapper"&gt;
        &lt;div class="media-recorder-default-recording-play-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-recording-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-play-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-timer"&gt;00:00/00:00&lt;/div&gt;
        &lt;div class="media-recorder-sound-intensity"&gt;
            &lt;div class="sound-intensity-large" id="sound-intensity-6"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-large" id="sound-intensity-5"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-medium" id="sound-intensity-4"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-medium" id="sound-intensity-3"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-low" id="sound-intensity-2"&gt;&lt;/div&gt;
            &lt;div class="sound-intensity-low" id="sound-intensity-1"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-dotted-sound-intensity"&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot short-dot"&gt;&lt;/div&gt;&lt;/div&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot tall-dot"&gt;&lt;/div&gt;&lt;/div&gt;
            &lt;div class="media-recorder-dot-container"&gt;&lt;div class="sound-intensity-dot short-dot"&gt;&lt;/div&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-progress-bar"&gt;
            &lt;div class="media-recorder-progress-bar-slider"&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="media-recorder-reset-button"&gt;&lt;/div&gt;
        &lt;div class="media-recorder-download-button"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div&gt;
        &lt;div class="media-recorder-reset-dialog"&gt;
            &lt;div class="dialog-text"&gt;&lt;/div&gt;
            &lt;div class="dialog-buttons"&gt;
                &lt;div class="confirm-button"&gt;&lt;/div&gt;
                &lt;div class="deny-button"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module &amp;&amp; module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 16);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
var CSS_CLASSES = exports.CSS_CLASSES = {
    PLAYER_WRAPPER: "media-recorder-player-wrapper",
    PLAYER_LOADER: "media-recorder-player-loader",
    AUDIO_LOADER: "audio-loader",
    WRAPPER: "media-recorder-wrapper",
    WRAPPER_BROWSER_NOT_SUPPORTED: "media-recorder-wrapper-browser-not-supported",
    DEFAULT_RECORDING_PLAY_BUTTON: "media-recorder-default-recording-play-button",
    RECORDING_BUTTON: "media-recorder-recording-button",
    PLAY_BUTTON: "media-recorder-play-button",
    RESET_BUTTON: "media-recorder-reset-button",
    DOWNLOAD_BUTTON: "media-recorder-download-button",
    PROGRESS_BAR: "media-recorder-progress-bar",
    PROGRESS_BAR_SLIDER: "media-recorder-progress-bar-slider",
    TIMER: "media-recorder-timer",
    SOUND_INTENSITY: "media-recorder-sound-intensity",
    DOTTED_SOUND_INTENSITY: "media-recorder-dotted-sound-intensity",
    TALL_DOT: "tall-dot",
    SHORT_DOT: "short-dot",
    SOUND_INTENSITY_DOT: "sound-intensity-dot",
    RESET_DIALOG: "media-recorder-reset-dialog",
    DIALOG_TEXT: "dialog-text",
    CONFIRM_BUTTON: "confirm-button",
    DENY_BUTTON: "deny-button",
    EXTENDED_MODE: "extended-mode",
    SELECTED: "selected",
    DISABLED: "disabled",
    DISABLE_RECORD_BUTTON: "disable-record-button"
};

/***/ }),
/* 1 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Button = exports.Button = function () {
    function Button($view) {
        _classCallCheck(this, Button);

        if (this.constructor === Button) throw new Error("Cannot create an instance of BasePlayer abstract class");

        this.$view = $view;
        this.$view.css("z-index", "100");
    }

    _createClass(Button, [{
        key: "activate",
        value: function activate() {
            var _this = this;

            this.$view.click(function (event) {
                if (!_this.isDoubleClick(event)) {
                    _this._eventHandler();
                }
            });
        }
    }, {
        key: "isDoubleClick",
        value: function isDoubleClick(event) {
            return event.originalEvent &amp;&amp; event.originalEvent.detail &gt; 1;
        }
    }, {
        key: "deactivate",
        value: function deactivate() {
            this.$view.unbind();
        }
    }, {
        key: "forceClick",
        value: function forceClick() {
            this.$view.click();
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.deactivate();
            this.$view.remove();
            this.$view = null;
        }
    }, {
        key: "_eventHandler",
        value: function _eventHandler() {
            throw new Error("EventHandler accessor is not implemented");
        }
    }]);

    return Button;
}();

/***/ }),
/* 2 */,
/* 3 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.RecordButton = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _Button2 = __webpack_require__(1);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var RecordButton = exports.RecordButton = function (_Button) {
    _inherits(RecordButton, _Button);

    function RecordButton(_ref) {
        var $view = _ref.$view,
            state = _ref.state;

        _classCallCheck(this, RecordButton);

        var _this = _possibleConstructorReturn(this, (RecordButton.__proto__ || Object.getPrototypeOf(RecordButton)).call(this, $view));

        _this._keyboardController = null;
        _this._timeoutID = null;

        _this.state = state;
        return _this;
    }

    _createClass(RecordButton, [{
        key: "destroy",
        value: function destroy() {
            _get(RecordButton.prototype.__proto__ || Object.getPrototypeOf(RecordButton.prototype), "destroy", this).call(this);
            this._keyboardController = null;
            this.state = null;
            this._clearTimeout();
        }
    }, {
        key: "reset",
        value: function reset() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onResetCallback();
            this._clearTimeout();
        }
    }, {
        key: "setUnclickView",
        value: function setUnclickView() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
        }
    }, {
        key: "_eventHandler",
        value: function _eventHandler() {
            if (this.state.isNew() || this.state.isLoaded() || this.state.isLoadedDefaultRecording() || this.state.isBlockedSafari()) this._startRecording();else if (this.state.isRecording()) this._stopRecording();
        }
    }, {
        key: "_startRecording",
        value: function _startRecording() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onStartRecordingCallback();
            this._handleDisablingRecordButton();
        }
    }, {
        key: "_stopRecording",
        value: function _stopRecording() {
            if (!this._timeoutID) {
                this.$view.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
                this.onStopRecordingCallback();
            }
        }
    }, {
        key: "_handleDisablingRecordButton",
        value: function _handleDisablingRecordButton() {
            this._disableRecordButton();
            _self = this;
            this._timeoutID = setTimeout(function () {
                _self._enableRecordButton();
            }, 1000);
        }
    }, {
        key: "_disableRecordButton",
        value: function _disableRecordButton() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.DISABLE_RECORD_BUTTON);
        }
    }, {
        key: "_enableRecordButton",
        value: function _enableRecordButton() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.DISABLE_RECORD_BUTTON);
            this._clearTimeout();
        }
    }, {
        key: "_clearTimeout",
        value: function _clearTimeout() {
            if (this._timeoutID) {
                clearTimeout(this._timeoutID);
                this._timeoutID = null;
            }
        }
    }, {
        key: "setKeyboardController",
        value: function setKeyboardController(keyboardController) {
            this._keyboardController = keyboardController;
        }
    }, {
        key: "onStartRecording",
        set: function set(callback) {
            this.onStartRecordingCallback = callback;
        }
    }, {
        key: "onStopRecording",
        set: function set(callback) {
            this.onStopRecordingCallback = callback;
        }
    }, {
        key: "onReset",
        set: function set(callback) {
            this.onResetCallback = callback;
        }
    }]);

    return RecordButton;
}(_Button2.Button);

/***/ }),
/* 4 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var BlobService = exports.BlobService = function () {
    function BlobService() {
        _classCallCheck(this, BlobService);
    }

    _createClass(BlobService, null, [{
        key: "serialize",
        value: function serialize(blob) {
            return new Promise(function (resolve) {
                var reader = new FileReader();
                if (blob instanceof Blob) {
                    var realFileReader = reader._realReader;
                    if (realFileReader) {
                        reader = realFileReader;
                    }
                }
                reader.onloadend = function () {
                    return resolve(reader.result);
                };
                reader.readAsDataURL(blob);
            });
        }
    }, {
        key: "deserialize",
        value: function deserialize(base64Data) {
            var mediaSourceData = base64Data.split(",");
            var recording = mediaSourceData[1];
            var contentType = mediaSourceData[0].replace(";base64", "").replace("data:", "");
            return this._b64toBlob(recording, contentType);
        }
    }, {
        key: "_b64toBlob",
        value: function _b64toBlob(b64Data, contentType, sliceSize) {
            contentType = contentType || '';
            sliceSize = sliceSize || 512;

            var byteCharacters = atob(b64Data);
            var byteArrays = [];

            for (var offset = 0; offset &lt; byteCharacters.length; offset += sliceSize) {
                var slice = byteCharacters.slice(offset, offset + sliceSize);

                var byteNumbers = new Array(slice.length);
                for (var i = 0; i &lt; slice.length; i++) {
                    byteNumbers[i] = slice.charCodeAt(i);
                }

                var byteArray = new Uint8Array(byteNumbers);

                byteArrays.push(byteArray);
            }

            return new Blob(byteArrays, { type: contentType });
        }
    }, {
        key: "getMp3BlobFromDecodedDataByWorker",
        value: function getMp3BlobFromDecodedDataByWorker(worker, decodedData) {
            var newSampleRate = 22050;
            var buffers = this.prepareSampleBuffers(decodedData);
            var left = this._interpolateArray(buffers[0], newSampleRate, decodedData.sampleRate);
            var right = this._interpolateArray(buffers[1], newSampleRate, decodedData.sampleRate);

            return worker.execute(decodedData.numberOfChannels, newSampleRate, decodedData.length, left, right);
        }
    }, {
        key: "getMp3BlobFromDecodedData",
        value: function getMp3BlobFromDecodedData(decodedData) {
            var newSampleRate = 22050;
            var buffers = this.prepareSampleBuffers(decodedData);
            var left = this._interpolateArray(buffers[0], newSampleRate, decodedData.sampleRate);
            var right = this._interpolateArray(buffers[1], newSampleRate, decodedData.sampleRate);

            return this._encode(decodedData.numberOfChannels, newSampleRate, decodedData.length, left, right);
        }
    }, {
        key: "_interpolateArray",
        value: function _interpolateArray(data, newSampleRate, oldSampleRate) {
            var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate));
            var newData = new Int16Array(fitCount - 1);
            var springFactor = Number((data.length - 1) / (fitCount - 1));
            newData[0] = data[0];
            for (var i = 1; i &lt; fitCount - 1; i++) {
                var tmp = i * springFactor;
                var before = Number(Math.floor(tmp)).toFixed();
                var after = Number(Math.ceil(tmp)).toFixed();
                var atPoint = tmp - before;
                newData[i] = this._linearInterpolate(data[before], data[after], atPoint);
            }
            newData[fitCount - 1] = data[data.length - 1];

            return newData;
        }
    }, {
        key: "_linearInterpolate",
        value: function _linearInterpolate(before, after, atPoint) {
            return before + (after - before) * atPoint;
        }
    }, {
        key: "prepareSampleBuffers",
        value: function prepareSampleBuffers(decodedData) {
            var left = this._convertFloat32ToInt16Array(decodedData.getChannelData(0));
            var right = left;
            if (decodedData.numberOfChannels === 2) {
                right = this._convertFloat32ToInt16Array(decodedData.getChannelData(1));
            }
            return [left, right];
        }
    }, {
        key: "_encode",
        value: function _encode(channels, sampleRate, sampleLen, left, right) {
            var buffer = [];
            var mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 96); //third value determinate bitrate

            var maxSamples = 1152;
            for (var i = 0; i &lt; sampleLen; i += maxSamples) {
                var leftChunk = left.subarray(i, i + maxSamples);
                var rightChunk = right.subarray(i, i + maxSamples);

                var mp3buf = mp3enc.encodeBuffer(leftChunk, rightChunk);
                if (mp3buf.length &gt; 0) {
                    buffer.push(new Int8Array(mp3buf));
                }
            }
            var d = mp3enc.flush();
            if (d.length &gt; 0) {
                buffer.push(new Int8Array(d));
            }

            var blob = new Blob(buffer, { type: 'audio/mpeg-3' });
            return blob;
        }

        //lamejs require int16 array
        //see more at - https://github.com/zhuker/lamejs/issues/10#issuecomment-141718262

    }, {
        key: "_convertFloat32ToInt16Array",
        value: function _convertFloat32ToInt16Array(data) {
            var len = data.length,
                i = 0;
            var dataAsInt16Array = new Int16Array(len);

            while (i &lt; len) {
                dataAsInt16Array[i] = convert(data[i++]);
            }
            function convert(n) {
                var v = n &lt; 0 ? n * 32768 : n * 32767; // convert in range [-32768, 32767]
                return Math.max(-32768, Math.min(32768, v)); // clamp
            }
            return dataAsInt16Array;
        }
    }]);

    return BlobService;
}();

/***/ }),
/* 5 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.SoundIntensity = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var SoundIntensity = exports.SoundIntensity = function () {
    function SoundIntensity($view) {
        _classCallCheck(this, SoundIntensity);

        this.$view = $view;
        this.volumeLevels = 6;
        this.interval = null;
        this.enableAnalyser = true;
        this.shouldBeVisible = true;
    }

    _createClass(SoundIntensity, [{
        key: 'setEnableAnalyser',
        value: function setEnableAnalyser(enableAnalyser) {
            this.enableAnalyser = enableAnalyser;
            if (this.shouldBeVisible) {
                if (this.enableAnalyser) {
                    this.show();
                } else {
                    this.hide();
                }
            }
        }
    }, {
        key: 'startAnalyzing',
        value: function startAnalyzing(analyser) {
            var _this = this;

            this.interval = setInterval(function () {
                return _this._updateIntensity(analyser);
            }, 100);
        }
    }, {
        key: 'stopAnalyzing',
        value: function stopAnalyzing() {
            if (this.interval) clearInterval(this.interval);
            this._clearIntensity();
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this.stopAnalyzing();
            this.interval = null;
            this.$view.remove();
            this.$view = null;
        }
    }, {
        key: 'show',
        value: function show() {
            if (this.enableAnalyser) {
                this.$view.css('display', '');
            }
            this.shouldBeVisible = true;
        }
    }, {
        key: 'hide',
        value: function hide() {
            this.$view.css('display', 'none');
            this.shouldBeVisible = false;
        }
    }, {
        key: 'setEventBus',
        value: function setEventBus(eventBus, sourceID) {
            this.eventBus = eventBus;
            this.sourceID = sourceID;
        }
    }, {
        key: '_updateIntensity',
        value: function _updateIntensity(analyser) {
            var frequencyArray = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(frequencyArray);
            var avgVolume = this._calculateAvgVolume(frequencyArray);
            var raisedVolume = this._raiseVolume(avgVolume);
            var alignedVolume = this._alignVolume(raisedVolume);
            var intensity = alignedVolume * this.volumeLevels;
            this._setIntensity(intensity);
            if (this.eventBus) {
                this._handleEvents(intensity);
            }
        }
    }, {
        key: '_handleEvents',
        value: function _handleEvents(intensity) {
            if (this.lastIntensityLevel === undefined) {
                this.lastIntensityLevel = 0;
                return;
            }
            var newIntensityLevel = Math.floor(intensity);
            if (newIntensityLevel !== this.lastIntensityLevel) {
                this.lastIntensityLevel = newIntensityLevel;
                var eventData = {
                    'source': this.sourceID,
                    'item': 'intensity',
                    'value': newIntensityLevel,
                    'score': ''
                };
                this.eventBus.sendEvent('ValueChanged', eventData);
            }
        }
    }, {
        key: '_calculateAvgVolume',
        value: function _calculateAvgVolume(volumeArray) {
            var sum = 0;
            var _iteratorNormalCompletion = true;
            var _didIteratorError = false;
            var _iteratorError = undefined;

            try {
                for (var _iterator = volumeArray[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
                    var i = _step.value;

                    sum += i;
                }
            } catch (err) {
                _didIteratorError = true;
                _iteratorError = err;
            } finally {
                try {
                    if (!_iteratorNormalCompletion &amp;&amp; _iterator.return) {
                        _iterator.return();
                    }
                } finally {
                    if (_didIteratorError) {
                        throw _iteratorError;
                    }
                }
            }

            return sum / volumeArray.length;
        }
    }, {
        key: '_raiseVolume',
        value: function _raiseVolume(volume) {
            return volume &gt; 0 ? volume * 1.2 : volume;
        }
    }, {
        key: '_alignVolume',
        value: function _alignVolume(volume) {
            volume = volume &gt; 0 ? volume : 0;
            volume = volume &lt; 64 ? volume : 64;
            return volume / 64;
        }
    }, {
        key: '_setIntensity',
        value: function _setIntensity(intensity) {
            this._clearIntensity();
            for (var currentLevel = 1; currentLevel &lt;= intensity; currentLevel++) {
                var levelId = "#sound-intensity-" + currentLevel;
                var $level = this.$view.find(levelId);
                $level.addClass(_CssClasses.CSS_CLASSES.SELECTED);
            }
        }
    }, {
        key: '_clearIntensity',
        value: function _clearIntensity() {
            for (var currentLevel = 1; currentLevel &lt;= this.volumeLevels; currentLevel++) {
                var levelId = "#sound-intensity-" + currentLevel;
                var $level = this.$view.find(levelId);
                $level.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
            }
        }
    }]);

    return SoundIntensity;
}();

/***/ }),
/* 6 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.BaseKeyboardController = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var BaseKeyboardController = exports.BaseKeyboardController = function (_KeyboardController) {
    _inherits(BaseKeyboardController, _KeyboardController);

    function BaseKeyboardController(elements, columnsCount, model, mediaState, activationState, speak, speakAndExecuteCallback) {
        _classCallCheck(this, BaseKeyboardController);

        var _this = _possibleConstructorReturn(this, (BaseKeyboardController.__proto__ || Object.getPrototypeOf(BaseKeyboardController)).call(this, elements, columnsCount));

        _this._isRecording = false;
        _this.DEFAULT_TTS_PHRASES = {
            DEFAULT_RECORDING_PLAY_BUTTON: "Default recording play button",
            RECORDING_BUTTON: "Recording button",
            PLAY_BUTTON: "Play button",
            RESET_BUTTON: "Reset button",
            DOWNLOAD_BUTTON: "Download button",
            RESET_DIALOG: "Reset dialog",
            START_RECORDING: "Start recording",
            STOP_RECORDING: "Recording stopped",
            DISABLED: "Disabled"
        };


        if (_this.constructor === BaseKeyboardController) throw new Error("Cannot create an instance of KeyboardController abstract class");

        _this._langTag = model.langAttribute;
        _this._mediaState = mediaState;
        _this._activationState = activationState;
        _this._speak = speak;
        _this._speakAndExecuteCallback = speakAndExecuteCallback;
        return _this;
    }

    _createClass(BaseKeyboardController, [{
        key: "setSpeechTexts",
        value: function setSpeechTexts(speechTexts) {
            this.speechTexts = {
                DefaultRecordingPlayButton: this.DEFAULT_TTS_PHRASES.DEFAULT_RECORDING_PLAY_BUTTON,
                RecordingButton: this.DEFAULT_TTS_PHRASES.RECORDING_BUTTON,
                PlayButton: this.DEFAULT_TTS_PHRASES.PLAY_BUTTON,
                ResetButton: this.DEFAULT_TTS_PHRASES.RESET_BUTTON,
                DownloadButton: this.DEFAULT_TTS_PHRASES.DOWNLOAD_BUTTON,
                ResetDialog: this.DEFAULT_TTS_PHRASES.RESET_DIALOG,
                StartRecording: this.DEFAULT_TTS_PHRASES.START_RECORDING,
                StopRecording: this.DEFAULT_TTS_PHRASES.STOP_RECORDING,
                Disabled: this.DEFAULT_TTS_PHRASES.DISABLED
            };

            if (!speechTexts || $.isEmptyObject(speechTexts)) {
                return;
            };

            this.speechTexts = {
                DefaultRecordingPlayButton: TTSUtils.getSpeechTextProperty(speechTexts.DefaultRecordingPlayButton.DefaultRecordingPlayButton, this.speechTexts.DefaultRecordingPlayButton),
                RecordingButton: TTSUtils.getSpeechTextProperty(speechTexts.RecordingButton.RecordingButton, this.speechTexts.RecordingButton),
                PlayButton: TTSUtils.getSpeechTextProperty(speechTexts.PlayButton.PlayButton, this.speechTexts.PlayButton),
                ResetButton: TTSUtils.getSpeechTextProperty(speechTexts.ResetButton.ResetButton, this.speechTexts.ResetButton),
                DownloadButton: TTSUtils.getSpeechTextProperty(speechTexts.DownloadButton.DownloadButton, this.speechTexts.DownloadButton),
                ResetDialog: TTSUtils.getSpeechTextProperty(speechTexts.ResetDialog.ResetDialog, this.speechTexts.ResetDialog),
                StartRecording: TTSUtils.getSpeechTextProperty(speechTexts.StartRecording.StartRecording, this.speechTexts.StartRecording),
                StopRecording: TTSUtils.getSpeechTextProperty(speechTexts.StopRecording.StopRecording, this.speechTexts.StopRecording),
                Disabled: TTSUtils.getSpeechTextProperty(speechTexts.Disabled.Disabled, this.speechTexts.Disabled)
            };
        }
    }, {
        key: "getTarget",
        value: function getTarget(element, willBeClicked) {
            return $(element);
        }
    }, {
        key: "switchElement",
        value: function switchElement(move) {
            _get(BaseKeyboardController.prototype.__proto__ || Object.getPrototypeOf(BaseKeyboardController.prototype), "switchElement", this).call(this, move);
            if (!this._isCurrentElementNotDisplayed()) {
                this.readCurrentElement();
            }
        }
    }, {
        key: "nextElement",
        value: function nextElement(event) {
            if (event) {
                event.preventDefault();
            }

            if (this._isKeyboardNavigationBlocked()) {
                return;
            }

            this.switchElement(1);

            if (this._isCurrentElementNotDisplayed()) {
                this.nextElement();
            }
        }
    }, {
        key: "previousElement",
        value: function previousElement(event) {
            if (event) {
                event.preventDefault();
            }

            if (this._isKeyboardNavigationBlocked()) {
                return;
            }

            this.switchElement(-1);

            if (this._isCurrentElementNotDisplayed()) {
                this.previousElement();
            }
        }
    }, {
        key: "nextRow",
        value: function nextRow(event) {
            if (event) {
                event.preventDefault();
            }

            if (this._isKeyboardNavigationBlocked()) {
                return;
            }

            this.switchElement(this.columnsCount);

            if (this._isCurrentElementNotDisplayed()) {
                this.nextRow();
            }
        }
    }, {
        key: "previousRow",
        value: function previousRow(event) {
            if (event) {
                event.preventDefault();
            }

            if (this._isKeyboardNavigationBlocked()) {
                return;
            }

            this.switchElement(-this.columnsCount);

            if (this._isCurrentElementNotDisplayed()) {
                this.previousRow();
            }
        }
    }, {
        key: "enter",
        value: function enter(event) {
            if (event) {
                event.preventDefault();
            }

            if (!this.keyboardNavigationActive) {
                this._performFirstEnterEvent();
            } else {
                this._performNotFirstEnterEvent();
            }
        }
    }, {
        key: "_performFirstEnterEvent",
        value: function _performFirstEnterEvent() {
            this.keyboardNavigationActive = true;

            if (this._isKeyboardNavigationBlocked()) {
                this._markActiveElement();
            } else {
                this._markAndReadFirstDisplayedElement();
            }
        }
    }, {
        key: "_markActiveElement",
        value: function _markActiveElement() {
            if (this._mediaState.isPlayingDefaultRecording()) {
                this.markDefaultRecordingPlayButton();
            } else if (this._mediaState.isRecording() || this._isRecording) {
                this.markRecordingButton();
            } else if (this._mediaState.isPlaying()) {
                this.markPlayButton();
            }
        }
    }, {
        key: "markDefaultRecordingPlayButton",
        value: function markDefaultRecordingPlayButton() {
            throw new Error("readElement method is not implemented");
        }
    }, {
        key: "markRecordingButton",
        value: function markRecordingButton() {
            throw new Error("readElement method is not implemented");
        }
    }, {
        key: "markPlayButton",
        value: function markPlayButton() {
            throw new Error("readElement method is not implemented");
        }
    }, {
        key: "_markAndReadFirstDisplayedElement",
        value: function _markAndReadFirstDisplayedElement() {
            this.markCurrentElement(0);
            if (this._isCurrentElementNotDisplayed()) {
                this.nextElement();
            } else {
                this.readCurrentElement();
            }
        }
    }, {
        key: "_performNotFirstEnterEvent",
        value: function _performNotFirstEnterEvent() {
            if (!this._isKeyboardNavigationBlocked()) {
                this.readCurrentElement();
            }
        }
    }, {
        key: "select",
        value: function select(event) {
            if (this._isAddonDisabled() &amp;&amp; !this._getCurrentElement().hasClass(_CssClasses.CSS_CLASSES.DIALOG_TEXT)) {
                var textVoiceObject = [];

                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);

                this._speak(textVoiceObject);
            }

            _get(BaseKeyboardController.prototype.__proto__ || Object.getPrototypeOf(BaseKeyboardController.prototype), "select", this).call(this, event);
        }
    }, {
        key: "exitWCAGMode",
        value: function exitWCAGMode() {
            this._isRecording = false;
            _get(BaseKeyboardController.prototype.__proto__ || Object.getPrototypeOf(BaseKeyboardController.prototype), "exitWCAGMode", this).call(this);
        }
    }, {
        key: "_isAddonDisabled",
        value: function _isAddonDisabled() {
            return this._activationState.isInactive();
        }
    }, {
        key: "_isKeyboardNavigationBlocked",
        value: function _isKeyboardNavigationBlocked() {
            return this._mediaState.isPlayingDefaultRecording() || this._mediaState.isRecording() || this._mediaState.isPlaying() || this._isRecording;
        }
    }, {
        key: "_isCurrentElementNotDisplayed",
        value: function _isCurrentElementNotDisplayed() {
            return this._getCurrentElement().style("display") === "none";
        }
    }, {
        key: "_getCurrentElement",
        value: function _getCurrentElement() {
            return this.getTarget(this.keyboardNavigationCurrentElement, false);
        }
    }, {
        key: "readCurrentElement",
        value: function readCurrentElement() {
            this.readElement(this.keyboardNavigationCurrentElement);
        }
    }, {
        key: "readElement",
        value: function readElement(element) {
            throw new Error("readElement method is not implemented");
        }
    }, {
        key: "isCurrentElementDisabled",
        value: function isCurrentElementDisabled() {
            return this._getCurrentElement().hasClass(_CssClasses.CSS_CLASSES.DISABLE_RECORD_BUTTON);
        }
    }, {
        key: "_speakRecordingButtonTTS",
        value: function _speakRecordingButtonTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.RecordingButton);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_speakPlayButtonTTS",
        value: function _speakPlayButtonTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.PlayButton);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "onStartRecording",
        value: function onStartRecording(callbackFunction) {
            this._isRecording = true;
            this._speakStartRecordingTTS(callbackFunction);
        }
    }, {
        key: "onStartRecordingWhenSoundEffect",
        value: function onStartRecordingWhenSoundEffect() {
            this._isRecording = true;
        }
    }, {
        key: "_speakStartRecordingTTS",
        value: function _speakStartRecordingTTS(callbackFunction) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.StartRecording);

            this._speakAndExecuteCallback(textVoiceObject, callbackFunction);
        }
    }, {
        key: "onStopRecording",
        value: function onStopRecording() {
            this._isRecording = false;
            this._speakStopRecordingTTS();
        }
    }, {
        key: "onStopRecordingWhenSoundEffect",
        value: function onStopRecordingWhenSoundEffect() {
            this._isRecording = false;
        }
    }, {
        key: "_speakStopRecordingTTS",
        value: function _speakStopRecordingTTS() {
            if (this.isCurrentElementDisabled()) return;

            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.StopRecording);

            this._speak(textVoiceObject);
        }
    }, {
        key: "_pushDisabledMessageToTextVoiceObject",
        value: function _pushDisabledMessageToTextVoiceObject(textVoiceObject) {
            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.Disabled);
        }
    }, {
        key: "_pushMessageToTextVoiceObjectWithLanguageFromLesson",
        value: function _pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, message) {
            this._pushMessageToTextVoiceObject(textVoiceObject, message, false);
        }
    }, {
        key: "_pushMessageToTextVoiceObjectWithLanguageFromPresenter",
        value: function _pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, message) {
            this._pushMessageToTextVoiceObject(textVoiceObject, message, true);
        }
    }, {
        key: "_pushMessageToTextVoiceObject",
        value: function _pushMessageToTextVoiceObject(textVoiceObject, message) {
            var usePresenterLangTag = arguments.length &gt; 2 &amp;&amp; arguments[2] !== undefined ? arguments[2] : false;

            if (usePresenterLangTag) {
                textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, this._langTag));
            } else {
                textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
            }
        }
    }]);

    return BaseKeyboardController;
}(KeyboardController);

/***/ }),
/* 7 */,
/* 8 */,
/* 9 */,
/* 10 */,
/* 11 */,
/* 12 */,
/* 13 */,
/* 14 */,
/* 15 */,
/* 16 */
/***/ (function(module, exports, __webpack_require__) {

var _MediaRecorder = __webpack_require__(17);

function AddonMedia_Recorder_create() {

    var presenter = function presenter() {};

    presenter.mediaRecorder = new _MediaRecorder.MediaRecorder();
    presenter.addonID = null;

    presenter.setPlayerController = function (controller) {
        presenter.mediaRecorder.setPlayerController(controller);
    };

    presenter.setEventBus = function (eventBus) {
        presenter.mediaRecorder.setEventBus(eventBus);
    };

    presenter.run = function run(view, model) {
        presenter.view = view;
        presenter.mediaRecorder.run(view, model);
        presenter.updateAddonID(model);
        handleDestroyEvent();
    };

    presenter.createPreview = function createPreview(view, model) {
        presenter.view = view;
        presenter.mediaRecorder.createPreview(view, model);
        presenter.updateAddonID(model);
        handleDestroyEvent();
    };

    presenter.updateAddonID = function (model) {
        if (model.hasOwnProperty('ID')) {
            presenter.addonID = model['ID'];
        }
    };

    presenter.isEmpty = function isEmpty() {
        return presenter.mediaRecorder.isEmpty();
    };

    presenter.getMP3File = function getMP3File() {
        return presenter.mediaRecorder.getMP3File();
    };

    presenter.getState = function getState() {
        return presenter.mediaRecorder.getState();
    };

    presenter.setState = function setState(state) {
        presenter.mediaRecorder.setState(state);
    };

    presenter.startRecording = function startRecording() {
        presenter.mediaRecorder.startRecording();
    };

    presenter.stopRecording = function stopRecording() {
        presenter.mediaRecorder.stopRecording();
    };

    presenter.startPlaying = function startPlaying() {
        presenter.mediaRecorder.startPlaying();
    };

    presenter.stopPlaying = function stopPlaying() {
        presenter.mediaRecorder.stopPlaying();
    };

    presenter.getErrorCount = function getErrorCount() {
        return 0;
    };

    presenter.getMaxScore = function getMaxScore() {
        return 0;
    };

    presenter.getScore = function getScore() {
        return 0;
    };

    presenter.show = function () {
        presenter.mediaRecorder.show();
    };

    presenter.hide = function () {
        presenter.mediaRecorder.hide();
    };

    presenter.setShowErrorsMode = function setShowErrorsMode() {
        if (!presenter.mediaRecorder.model.enableInErrorCheckingMode) {
            presenter.mediaRecorder.deactivate();
        }
    };

    presenter.setWorkMode = function setWorkMode() {
        presenter.mediaRecorder.activate();
    };

    presenter.setWCAGStatus = function (isWCAGOn) {
        presenter.mediaRecorder.setWCAGStatus(isWCAGOn);
    };

    presenter.reset = function reset() {
        presenter.mediaRecorder.reset();
    };

    presenter.enable = function enable() {
        presenter.mediaRecorder.enable();
    };

    presenter.isEnabledInGSAMode = function () {
        return true;
    };

    presenter.disable = function disable() {
        presenter.mediaRecorder.disable();
    };

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.mediaRecorder.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    presenter.executeCommand = function executeCommand(name, params) {
        var commands = {
            'startRecording': presenter.startRecording,
            'stopRecording': presenter.stopRecording,
            'startPlaying': presenter.startPlaying,
            'stopPlaying': presenter.stopPlaying,
            'setShowErrorsMode': presenter.setShowErrorsMode,
            'setWorkMode': presenter.setWorkMode,
            'reset': presenter.reset,
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enable,
            'disable': presenter.disable
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.destroy = function destroy(event) {
        if (event.target === presenter.view) {
            presenter.mediaRecorder.destroy();
            event.target = null;
            presenter.mediaRecorder = null;
            presenter.validateModel = null;
        }
    };

    presenter.preDestroy = function () {
        presenter.mediaRecorder.preDestroy();
    };

    presenter._internalElements = function () {
        return this.mediaRecorder._internalElements();
    };

    presenter._internalUpgradeModel = function (model) {
        return this.mediaRecorder._upgradeModel(model);
    };

    function handleDestroyEvent() {
        MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    }

    return presenter;
}

window.AddonMedia_Recorder_create = AddonMedia_Recorder_create;

/***/ }),
/* 17 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.MediaRecorder = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _validateModel = __webpack_require__(18);

var _ActivationState = __webpack_require__(20);

var _MediaState = __webpack_require__(21);

var _Errors = __webpack_require__(22);

var _PlayButton = __webpack_require__(23);

var _RecordButton = __webpack_require__(3);

var _ResetButton = __webpack_require__(24);

var _ResetDialog = __webpack_require__(25);

var _DownloadButton = __webpack_require__(26);

var _Timer = __webpack_require__(27);

var _ProgressBar = __webpack_require__(28);

var _AddonState = __webpack_require__(29);

var _RecordingTimeLimiter = __webpack_require__(30);

var _SoundIntensity = __webpack_require__(5);

var _DottedSoundIntensity = __webpack_require__(31);

var _MediaAnalyserService = __webpack_require__(32);

var _AudioLoader = __webpack_require__(34);

var _SoundEffect = __webpack_require__(36);

var _RecordButtonSoundEffect = __webpack_require__(37);

var _AddonViewService = __webpack_require__(38);

var _AudioResourcesProvider = __webpack_require__(39);

var _AudioRecorder = __webpack_require__(41);

var _AudioPlayer = __webpack_require__(44);

var _DefaultRecordingPlayButton = __webpack_require__(47);

var _DefaultKeyboardController = __webpack_require__(48);

var _ExtendedKeyboardController = __webpack_require__(49);

var _CssClasses = __webpack_require__(0);

var _MP3ConvertHandler = __webpack_require__(50);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var MediaRecorder = exports.MediaRecorder = function () {
    function MediaRecorder() {
        _classCallCheck(this, MediaRecorder);

        this.enableAnalyser = true;
        this.isMlibro = false;
        this.isWCAGOn = false;
        this.keyboardControllerObject = null;
        this.mp3ConvertHandler = null;
    }

    _createClass(MediaRecorder, [{
        key: "run",
        value: function run(view, model) {
            var upgradedModel = this._upgradeModel(model);
            var validatedModel = (0, _validateModel.validateModel)(upgradedModel);

            if (this._isBrowserNotSupported()) {
                this._showBrowserError(view);
            } else if (validatedModel.isValid) {
                this.mp3ConvertHandler = new _MP3ConvertHandler.MP3ConvertHandler(this.playerController);
                this._runAddon(view, validatedModel.value);
            } else {
                this._showError(view, validatedModel);
            }

            this._executeNotification(JSON.stringify({ type: "platform", target: this.model.ID }));
            this._buildKeyboardController();
            this.keyboardControllerObject.setSpeechTexts(upgradedModel['speechTexts']);
            this.recordButton.setKeyboardController(this.keyboardControllerObject);
        }
    }, {
        key: "createPreview",
        value: function createPreview(view, model) {
            var upgradedModel = this._upgradeModel(model);
            var validatedModel = (0, _validateModel.validateModel)(upgradedModel);

            if (!validatedModel.isValid) this._showError(view, validatedModel);else this._updatePreview(view, validatedModel);
        }
    }, {
        key: "setPlayerController",
        value: function setPlayerController(playerController) {
            this.playerController = playerController;
            if (this.player &amp;&amp; this.recorder) this._loadEventBus();

            var context = playerController.getContextMetadata();
            this.isMlibro = false;
            if (context != null &amp;&amp; "ismLibro" in context) {
                this.isMlibro = context["ismLibro"];
            }
        }
    }, {
        key: "isEmpty",
        value: function isEmpty() {
            return this.addonState.isEmpty();
        }
    }, {
        key: "getMP3File",
        value: function getMP3File() {
            return this.addonState.getMP3File();
        }
    }, {
        key: "getState",
        value: function getState() {
            return this.addonState.getState();
        }
    }, {
        key: "setState",
        value: function setState(state) {
            var _this = this;

            Object.assign(this.addonState, JSON.parse(state));
            this.addonState.getRecordingBlob().then(function (blob) {
                _this.mediaState.setLoading();
                var recording = void 0;
                if (_this.addonState.isMP3Format(blob)) {
                    try {
                        if (_this.player._isMobileSafari() || _this.player._isIosMlibro() || _this.player._isIOSWebViewUsingAppleWebKit()) {
                            var tmpBlob = new Blob([blob], { type: "audio/mpeg" });
                            recording = URL.createObjectURL(tmpBlob);
                        } else {
                            var tmpFile = new File([blob], "recording.mp3", { type: "audio/mp3" });
                            recording = URL.createObjectURL(tmpFile);
                        }
                    } catch (error) {
                        recording = URL.createObjectURL(blob);
                    }
                } else {
                    recording = URL.createObjectURL(blob);
                }
                _this.player.setRecording(recording);
                if (_this.model.extendedMode) {
                    _this.setEMRecordedStateView();
                }
            });
            this.addonState.getVisibility().then(function (isVisible) {
                _this.setVisibility(isVisible);
            });
            this.addonState.getEnabled().then(function (isEnable) {
                _this._setEnableState(isEnable);
            });
        }
    }, {
        key: "startRecording",
        value: function startRecording() {
            if (this.mediaState.isNew() || this.mediaState.isLoaded()) this.recordButton.forceClick();
        }
    }, {
        key: "stopRecording",
        value: function stopRecording() {
            if (this.mediaState.isRecording()) this.recordButton.forceClick();
        }
    }, {
        key: "startPlaying",
        value: function startPlaying() {
            if (this.mediaState.isLoaded()) this.playButton.forceClick();
        }
    }, {
        key: "stopPlaying",
        value: function stopPlaying() {
            if (this.mediaState.isPlaying()) this.playButton.forceClick();
        }
    }, {
        key: "preDestroy",
        value: function preDestroy() {
            if (this.recorder &amp;&amp; this.addonState != null &amp;&amp; this.addonState.isEmpty()) {
                this.recorder.sendPreDestroyedEmptyEvent();
            }
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.playButton.destroy();
            this.defaultRecordingPlayButton.destroy();
            this.recordButton.destroy();
            this.recorder.destroy();
            this.player.destroy();
            this.resourcesProvider.destroy();
            this.recordingTimeLimiter.destroy();
            this.soundIntensity.destroy();
            this.timer.destroy();
            this.startRecordingSoundEffect.destroy();
            this.stopRecordingSoundEffect.destroy();
            this.loader.destroy();
            this.addonViewService.destroy();
            this.mediaAnalyserService.destroy();
            this.addonState.destroy();
            this.mediaState.destroy();
            this.activationState.destroy();
            this.mp3ConvertHandler.destroy();

            this.viewHandlers = null;
            this.defaultRecordingPlayButton = null;
            this.recorder = null;
            this.player = null;
            this.resourcesProvider = null;
            this.recordingTimeLimiter = null;
            this.soundIntensity = null;
            this.timer = null;
            this.recordButton = null;
            this.playButton = null;
            this.downloadButton = null;
            this.resetButton = null;
            this.progressBar = null;
            this.stopRecordingSoundEffect = null;
            this.startRecordingSoundEffect = null;
            this.loader = null;
            this.addonViewService = null;
            this.mediaAnalyserService = null;
            this.addonState = null;
            this.mediaState = null;
            this.activationState = null;
            this.extendedModeButtonList = null;

            this.playerController = null;
            this.keyboardControllerObject = null;
            this.mp3ConvertHandler = null;
            this.view = null;
            this.model = null;
        }
    }, {
        key: "enable",
        value: function enable() {
            this._setEnableState(true);
        }
    }, {
        key: "disable",
        value: function disable() {
            this._setEnableState(false);
        }
    }, {
        key: "activate",
        value: function activate() {
            if (this.activationState.isInactive()) {
                this.activationState.setActive();
                this.addonViewService.activate();
                this._activateButtons();
            }
        }
    }, {
        key: "deactivate",
        value: function deactivate() {
            this._stopActions();
            this._deactivateButtons();
            this.activationState.setInactive();
            this.addonViewService.deactivate();
        }
    }, {
        key: "reset",
        value: function reset() {
            this.deactivate();
            if (this.model.isResetRemovesRecording) {
                this.resetRecording();
            }
            this.activate();
            this.setVisibility(this.model["Is Visible"]);
            this._setEnableState(!this.model.isDisabled);
        }
    }, {
        key: "resetRecording",
        value: function resetRecording() {
            this.recordButton.reset();
            this.player.reset();
            this.addonState.reset();
            this.timer.reset();
            if (this.defaultRecordingPlayer.hasRecording) {
                this.mediaState.setLoadedDefaultRecording();
                this.timer.setDuration(this.defaultRecordingPlayer.duration);
            } else this.mediaState.setNew();
            if (this.recorder) this.recorder.sendValueChangedEmptyEvent();
        }
    }, {
        key: "show",
        value: function show() {
            this.setVisibility(true);
            this.addonState.setVisibility(true);
        }
    }, {
        key: "hide",
        value: function hide() {
            this.setVisibility(false);
            this.addonState.setVisibility(false);
        }
    }, {
        key: "setVisibility",
        value: function setVisibility(isVisible) {
            this.addonViewService.setVisibility(isVisible);
        }
    }, {
        key: "_runAddon",
        value: function _runAddon(view, model) {
            this._loadAddon(view, model);
            this._loadLogic();
            this._loadDefaultRecording(this.model);
            this._activateButtons();
            this._loadWebViewMessageListener();
            this.setVisibility(model["Is Visible"]);
            this._setEnableState(!model.isDisabled);
        }
    }, {
        key: "_loadAddon",
        value: function _loadAddon(view, model) {
            this._loadCoreElements(view, model);

            this.mediaAnalyserService = new _MediaAnalyserService.MediaAnalyserService();
            this.recordingTimeLimiter = new _RecordingTimeLimiter.RecordingTimeLimiter(this.model.maxTime);

            this._loadMediaElements();
            this._loadViewElements();
        }
    }, {
        key: "_loadCoreElements",
        value: function _loadCoreElements(view, model) {
            this.view = view;
            this.model = model;
            this.viewHandlers = this._loadViewHandlers(this.view);
            if (this.model.extendedMode) {
                this._prepareExtendedModeView();
                this.setEMDefaultStateView();
            }
            if (this.model.disableRecording) {
                this._hidePlayAndTimerWidgets();
            }
            this.mediaState = new _MediaState.MediaState();
            this.activationState = new _ActivationState.ActivationState();
            this.addonState = new _AddonState.AddonState();
            this.addonState.setMP3ConvertHandler(this.mp3ConvertHandler);
        }
    }, {
        key: "_loadViewHandlers",
        value: function _loadViewHandlers(view) {
            return {
                $wrapperView: $(view).find("." + _CssClasses.CSS_CLASSES.WRAPPER),
                $playerView: $(view).find("." + _CssClasses.CSS_CLASSES.PLAYER_WRAPPER),
                $loaderView: $(view).find("." + _CssClasses.CSS_CLASSES.PLAYER_LOADER),
                $defaultRecordingPlayButtonView: $(view).find("." + _CssClasses.CSS_CLASSES.DEFAULT_RECORDING_PLAY_BUTTON),
                $recordButtonView: $(view).find("." + _CssClasses.CSS_CLASSES.RECORDING_BUTTON),
                $playButtonView: $(view).find("." + _CssClasses.CSS_CLASSES.PLAY_BUTTON),
                $timerView: $(view).find("." + _CssClasses.CSS_CLASSES.TIMER),
                $soundIntensityView: $(view).find("." + _CssClasses.CSS_CLASSES.SOUND_INTENSITY),
                $dottedSoundIntensityView: $(view).find("." + _CssClasses.CSS_CLASSES.DOTTED_SOUND_INTENSITY),
                $progressBarWrapperView: $(view).find("." + _CssClasses.CSS_CLASSES.PROGRESS_BAR),
                $progressBarSliderView: $(view).find("." + _CssClasses.CSS_CLASSES.PROGRESS_BAR_SLIDER),
                $resetButtonView: $(view).find("." + _CssClasses.CSS_CLASSES.RESET_BUTTON),
                $downloadButtonView: $(view).find("." + _CssClasses.CSS_CLASSES.DOWNLOAD_BUTTON),
                $resetDialogView: $(view).find("." + _CssClasses.CSS_CLASSES.RESET_DIALOG)
            };
        }
    }, {
        key: "_prepareExtendedModeView",
        value: function _prepareExtendedModeView() {
            this.viewHandlers.$wrapperView.addClass(_CssClasses.CSS_CLASSES.EXTENDED_MODE);
            this.viewHandlers.$timerView.insertBefore(this.viewHandlers.$playButtonView);
        }
    }, {
        key: "_hidePlayAndTimerWidgets",
        value: function _hidePlayAndTimerWidgets() {
            this.viewHandlers.$playButtonView.hide();
            this.viewHandlers.$defaultRecordingPlayButtonView.hide();
            this.viewHandlers.$timerView.hide();
        }
    }, {
        key: "_loadMediaElements",
        value: function _loadMediaElements() {
            this.recorder = new _AudioRecorder.AudioRecorder();
            this.player = new _AudioPlayer.AudioPlayer(this.viewHandlers.$playerView, this.isMlibro);
            this.player.setIsMlibro(this.isMlibro);
            this.defaultRecordingPlayer = new _AudioPlayer.AudioPlayer(this.viewHandlers.$playerView, this.isMlibro);
            this.resourcesProvider = new _AudioResourcesProvider.AudioResourcesProvider(this.viewHandlers.$wrapperView);
            if (this.eventBus) this._loadEventBus();
        }
    }, {
        key: "setEventBus",
        value: function setEventBus(eventBus) {
            this.eventBus = eventBus;
        }
    }, {
        key: "_loadEventBus",
        value: function _loadEventBus() {
            // this method passes the event bus to the other components
            this.player.setEventBus(this.eventBus, this.model.ID, "player");
            this.defaultRecordingPlayer.setEventBus(this.eventBus, this.model.ID, "default");
            this.recorder.setEventBus(this.eventBus, this.model.ID);
        }
    }, {
        key: "_loadViewElements",
        value: function _loadViewElements() {
            this.addonViewService = new _AddonViewService.AddonViewService(this.viewHandlers.$wrapperView);
            this.recordButton = this._loadRecordButton();

            this.defaultRecordingPlayButton = new _DefaultRecordingPlayButton.DefaultRecordingPlayButton({
                $view: this.viewHandlers.$defaultRecordingPlayButtonView,
                state: this.mediaState,
                defaultRecording: this.model.defaultRecording
            });

            this.playButton = new _PlayButton.PlayButton({
                $view: this.viewHandlers.$playButtonView,
                state: this.mediaState
            });

            this.extendedModeButtonList = [];
            if (this.model.extendedMode) {
                this.downloadButton = new _DownloadButton.DownloadButton({
                    $view: this.viewHandlers.$downloadButtonView,
                    addonState: this.addonState
                });
                this.resetButton = new _ResetButton.ResetButton(this.viewHandlers.$resetButtonView);
                this.resetDialog = new _ResetDialog.ResetDialog(this.viewHandlers.$resetDialogView, this.model.resetDialogLabels);
                this.progressBar = new _ProgressBar.ProgressBar(this.viewHandlers.$progressBarWrapperView);
                this.extendedModeButtonList.push(this.downloadButton);
                this.extendedModeButtonList.push(this.resetButton);
            }

            this.loader = new _AudioLoader.AudioLoader(this.viewHandlers.$loaderView);

            this.timer = new _Timer.Timer(this.viewHandlers.$timerView);
            if (this.model.extendedMode) {
                this.soundIntensity = new _DottedSoundIntensity.DottedSoundIntensity(this.viewHandlers.$dottedSoundIntensityView);
                this.viewHandlers.$soundIntensityView.css('display', 'none');
            } else {
                this.soundIntensity = new _SoundIntensity.SoundIntensity(this.viewHandlers.$soundIntensityView);
                this.viewHandlers.$dottedSoundIntensityView.css('display', 'none');
            }
            if (this.eventBus &amp;&amp; this.model.enableIntensityChangeEvents) {
                this.soundIntensity.setEventBus(this.eventBus, this.model.ID);
            }
            this.soundIntensity.setEnableAnalyser(this.enableAnalyser);

            this._hideSelectedElements();
        }
    }, {
        key: "_loadRecordButton",
        value: function _loadRecordButton() {
            var recordButton = new _RecordButton.RecordButton({
                $view: this.viewHandlers.$recordButtonView,
                state: this.mediaState
            });

            this.startRecordingSoundEffect = new _SoundEffect.SoundEffect(this.model.startRecordingSound, this.viewHandlers.$playerView);
            this.stopRecordingSoundEffect = new _SoundEffect.SoundEffect(this.model.stopRecordingSound, this.viewHandlers.$playerView);

            return new _RecordButtonSoundEffect.RecordButtonSoundEffect(recordButton, this.startRecordingSoundEffect, this.stopRecordingSoundEffect);
        }
    }, {
        key: "setEMDefaultStateView",
        value: function setEMDefaultStateView() {
            this.viewHandlers.$defaultRecordingPlayButtonView.css('display', 'none');
            this.viewHandlers.$recordButtonView.css('display', '');
            if (this.model.disableRecording) {
                this.viewHandlers.$timerView.css('display', 'none');
            } else {
                this.viewHandlers.$timerView.css('display', '');
            }
            if (this.soundIntensity) {
                this.soundIntensity.show();
            }
            this.viewHandlers.$playButtonView.css('display', 'none');
            this.viewHandlers.$progressBarWrapperView.css('display', 'none');
            this.viewHandlers.$progressBarWrapperView.css('visibility', 'hidden');
            this.viewHandlers.$resetButtonView.css('display', 'none');
            this.viewHandlers.$downloadButtonView.css('display', 'none');
        }
    }, {
        key: "setEMRecordedStateView",
        value: function setEMRecordedStateView() {
            this.viewHandlers.$defaultRecordingPlayButtonView.css('display', 'none');
            this.viewHandlers.$recordButtonView.css('display', 'none');
            this.viewHandlers.$timerView.css('display', '');
            if (this.soundIntensity) {
                this.soundIntensity.hide();
            }
            this.viewHandlers.$playButtonView.css('display', '');
            this.viewHandlers.$progressBarWrapperView.css('display', 'block');
            this.viewHandlers.$progressBarWrapperView.css('visibility', 'visible');
            this.viewHandlers.$resetButtonView.css('display', 'block');
            this.viewHandlers.$downloadButtonView.css('display', 'block');
        }
    }, {
        key: "setEMPlayingStateView",
        value: function setEMPlayingStateView() {
            this.viewHandlers.$defaultRecordingPlayButtonView.css('display', 'none');
            this.viewHandlers.$recordButtonView.css('display', 'none');
            this.viewHandlers.$timerView.css('display', '');
            if (this.soundIntensity) {
                this.soundIntensity.hide();
            }
            this.viewHandlers.$playButtonView.css('display', '');
            this.viewHandlers.$progressBarWrapperView.css('display', 'block');
            this.viewHandlers.$progressBarWrapperView.css('visibility', 'visible');
            this.viewHandlers.$resetButtonView.css('display', 'block');
            this.viewHandlers.$downloadButtonView.css('display', 'block');
        }
    }, {
        key: "_loadLogic",
        value: function _loadLogic() {
            var _this2 = this;

            this.recordButton.onStartRecording = function () {
                _this2.mediaState.setBlocked();
                if (_this2.platform === 'mlibro') {
                    _this2._handleMlibroStartRecording();
                } else {
                    _this2.resourcesProvider.getMediaResources().then(function (stream) {
                        var isSafari = window.DevicesUtils.getBrowserVersion().toLowerCase().indexOf("safari") &gt; -1;
                        if (isSafari) {
                            _this2.mediaState.setBlockedSafari();
                        }
                        _this2._handleRecording(stream);
                    });
                }
            };

            this.recordButton.onStopRecording = function () {
                if (_this2.platform === 'mlibro') _this2._handleMlibroStopRecording();else {
                    _this2.mediaState.setLoading();
                    _this2.timer.stopCountdown();
                    _this2.recordingTimeLimiter.stopCountdown();
                    _this2.soundIntensity.stopAnalyzing();
                    if (_this2.enableAnalyser) {
                        _this2.mediaAnalyserService.closeAnalyzing();
                    }
                    if (!_this2.model.disableRecording) {
                        _this2.recorder.stopRecording().then(function (blob) {
                            _this2.addonState.setRecordingBlob(blob);
                            var recording = URL.createObjectURL(blob);
                            _this2.player.reset();
                            _this2.player.setRecording(recording);

                            return blob;
                        }).then(function (wavBlob) {
                            return _this2.addonState.convertWavBlobToMP3BlobByWorker(wavBlob);
                        }).then(function (mp3Blob) {
                            _this2.addonState.setRecordingBlob(mp3Blob);
                        }, function () {});
                    }
                    _this2.resourcesProvider.destroy();
                }
                if (_this2.model.extendedMode) {
                    if (_this2.model.disableRecording) {
                        _this2.setEMDefaultStateView();
                    } else {
                        _this2.setEMRecordedStateView();
                    }
                }
                if (_this2.model.disableRecording) {
                    _this2.mediaState.setLoaded();
                }
            };

            this.recordButton.onReset = function () {
                _this2.mediaState.setLoading();
                _this2.timer.stopCountdown();
                _this2.recordingTimeLimiter.stopCountdown();
                _this2.soundIntensity.stopAnalyzing();
                if (_this2.enableAnalyser) {
                    _this2.mediaAnalyserService.closeAnalyzing();
                }
                if (_this2.recorder.recorder) {
                    _this2.recorder.stopRecording();
                }
                _this2.resourcesProvider.destroy();
            };

            if (this.model.extendedMode) {
                this.resetButton.onReset = function () {
                    _this2.resetDialog.open();
                    _this2.keyboardControllerObject.setElements(_this2._getElementsForResetDialogKeyboardNavigation());
                    if (_this2.keyboardControllerObject.keyboardNavigationActive) {
                        _this2.keyboardControllerObject.markDialogTextAndReadResetDialogTTS();
                    }
                };
                this.resetDialog.onConfirm = function () {
                    _this2.resetRecording();
                    if (_this2.model.extendedMode) {
                        _this2.setEMDefaultStateView();
                    }
                    _this2.progressBar.setProgress(0.0);
                    _this2.keyboardControllerObject.setElements(_this2._getElementsForExtendedKeyboardNavigation());
                    if (_this2.keyboardControllerObject.keyboardNavigationActive) {
                        _this2.keyboardControllerObject.markRecordingButton();
                        _this2.keyboardControllerObject.readCurrentElement();
                    }
                };
                this.resetDialog.onDeny = function () {
                    _this2.keyboardControllerObject.setElements(_this2._getElementsForExtendedKeyboardNavigation());
                    if (_this2.keyboardControllerObject.keyboardNavigationActive) {
                        _this2.keyboardControllerObject.markResetButton();
                        _this2.keyboardControllerObject.readCurrentElement();
                    }
                };

                this.progressBar.onStartDragging = function () {
                    if (_this2.mediaState.isPlaying()) {
                        _this2.player.pausePlaying();
                        _this2.playButton.forceClick();
                    }
                };

                this.progressBar.onStopDragging = function (progress) {
                    _this2.player.setProgress(progress);
                };
            }

            this.playButton.onStartPlaying = function () {
                _this2.mediaState.setPlaying();
                if (_this2.enableAnalyser) {
                    _this2.player.startPlaying().then(function (htmlMediaElement) {
                        return _this2.mediaAnalyserService.createAnalyserFromElement(htmlMediaElement).then(function (analyser) {
                            return _this2.soundIntensity.startAnalyzing(analyser);
                        });
                    });
                } else {
                    _this2.player.startPlaying();
                }
            };

            this.playButton.onStopPlaying = function () {
                _this2.mediaState.setLoaded();
                if (_this2.model.extendedMode) {
                    _this2.player.pausePlaying();
                } else {
                    _this2.player.stopPlaying();
                }
                _this2.soundIntensity.stopAnalyzing();
                if (_this2.enableAnalyser) {
                    _this2.mediaAnalyserService.closeAnalyzing();
                }
            };

            this.defaultRecordingPlayButton.onStartPlaying = function () {
                _this2.mediaState.setPlayingDefaultRecording();
                _this2.timer.setDuration(_this2.defaultRecordingPlayer.duration);
                _this2.timer.startCountdown();
                if (_this2.enableAnalyser) {
                    _this2.defaultRecordingPlayer.startPlaying().then(function (htmlMediaElement) {
                        return _this2.mediaAnalyserService.createAnalyserFromElement(htmlMediaElement).then(function (analyser) {
                            return _this2.soundIntensity.startAnalyzing(analyser);
                        });
                    });
                } else {
                    _this2.defaultRecordingPlayer.startPlaying();
                }
            };

            this.defaultRecordingPlayButton.onStopPlaying = function () {
                if (_this2.player.hasRecording) {
                    _this2.mediaState.setLoaded();
                    _this2.timer.setDuration(_this2.player.duration);
                } else {
                    _this2.mediaState.setLoadedDefaultRecording();
                }

                _this2.defaultRecordingPlayer.stopPlaying();
                _this2.timer.stopCountdown();
                _this2.soundIntensity.stopAnalyzing();
                if (_this2.enableAnalyser) {
                    _this2.mediaAnalyserService.closeAnalyzing();
                }
            };

            this.player.onStartLoading = function () {
                _this2.mediaState.setLoading();
                _this2.loader.show();
            };

            this.player.onEndLoading = function () {
                if (_this2.mediaState.isLoading()) {
                    _this2.mediaState.setLoaded();
                    _this2.loader.hide();
                }
            };

            var player = this.player;
            var timer = this.timer;
            var progressBar = this.progressBar;
            function timeUpdateCallback(event) {
                var currentTime = player.getCurrentTime();
                timer.setTime(currentTime);
                if (progressBar) {
                    player._getDuration().then(function (duration) {
                        progressBar.setProgress(currentTime / duration);
                    });
                }
            }

            this.player.onDurationChange = function (duration) {
                return _this2.timer.setDuration(duration);
            };
            this.player.onTimeUpdate = function (event) {
                return timeUpdateCallback(event);
            };
            this.player.onEndPlaying = function () {
                return _this2.playButton.forceClick();
            };

            this.defaultRecordingPlayer.onStartLoading = function () {
                _this2.mediaState.setLoading();
                _this2.loader.show();
            };

            this.defaultRecordingPlayer.onEndLoading = function () {
                if (_this2.player.hasRecording) {
                    _this2.mediaState.setLoaded();
                } else {
                    _this2.mediaState.setLoadedDefaultRecording();
                }
                _this2.loader.hide();
            };

            this.defaultRecordingPlayer.onDurationChange = function (duration) {
                return _this2.timer.setDuration(duration);
            };
            this.defaultRecordingPlayer.onEndPlaying = function () {
                return _this2.defaultRecordingPlayButton.forceClick();
            };

            this.recordingTimeLimiter.onTimeExpired = function () {
                return _this2.recordButton.forceClick();
            };
        }
    }, {
        key: "_handleRecording",
        value: function _handleRecording(stream) {
            var _this3 = this;

            this.mediaState.setRecording();
            if (!this.model.disableRecording) {
                this.recorder.startRecording(stream);
                this.timer.reset();
                this.timer.startDecrementalCountdown(this.recordingTimeLimiter.maxTime);
                this.recordingTimeLimiter.startCountdown();
            }
            if (this.enableAnalyser) {
                this.mediaAnalyserService.createAnalyserFromStream(stream).then(function (analyser) {
                    return _this3.soundIntensity.startAnalyzing(analyser);
                });
            }
        }
    }, {
        key: "_loadDefaultRecording",
        value: function _loadDefaultRecording(model) {
            if (_isValid(model.defaultRecording) &amp;&amp; model.isShowedDefaultRecordingButton) {
                this.mediaState.setLoading();
                this.defaultRecordingPlayer.setRecording(model.defaultRecording);
            }

            function _isValid(recording) {
                return recording != "" &amp;&amp; recording != null &amp;&amp; typeof recording != "undefined";
            }
        }
    }, {
        key: "_activateButtons",
        value: function _activateButtons() {
            this.recordButton.activate();
            if (!this.model.disableRecording) {
                this.playButton.activate();
            }
            this.defaultRecordingPlayButton.activate();
            if (this.model.extendedMode) {
                for (var i = 0; i &lt; this.extendedModeButtonList.length; i++) {
                    this.extendedModeButtonList[i].activate();
                }
            }
        }
    }, {
        key: "_deactivateButtons",
        value: function _deactivateButtons() {
            this.recordButton.deactivate();
            this.playButton.deactivate();
            this.defaultRecordingPlayButton.deactivate();
            if (this.model.extendedMode) {
                for (var i = 0; i &lt; this.extendedModeButtonList.length; i++) {
                    this.extendedModeButtonList[i].deactivate();
                }
            }
        }
    }, {
        key: "_stopActions",
        value: function _stopActions() {
            if (this.mediaState.isRecording()) {
                this.recordButton.forceClick();
            }
            if (this.mediaState.isPlaying()) {
                this.playButton.forceClick();
            }
            if (this.mediaState.isPlayingDefaultRecording()) {
                this.defaultRecordingPlayButton.forceClick();
            }
            if (this.mediaState.isLoaded()) {
                this.timer.setTime(0);
            }
        }
    }, {
        key: "_internalElements",
        value: function _internalElements() {
            return {
                validateModel: _validateModel.validateModel,
                ActivationState: _ActivationState.ActivationState,
                AddonState: _AddonState.AddonState,
                AudioLoader: _AudioLoader.AudioLoader,
                PlayButton: _PlayButton.PlayButton,
                RecordButton: _RecordButton.RecordButton,
                RecordingTimeLimiter: _RecordingTimeLimiter.RecordingTimeLimiter,
                MediaState: _MediaState.MediaState,
                Timer: _Timer.Timer,
                AudioPlayer: _AudioPlayer.AudioPlayer,
                DownloadButton: _DownloadButton.DownloadButton,
                SoundIntensity: _SoundIntensity.SoundIntensity,
                MP3ConvertHandler: _MP3ConvertHandler.MP3ConvertHandler
            };
        }
    }, {
        key: "_showError",
        value: function _showError(view, validatedModel) {
            DOMOperationsUtils.showErrorMessage(view, _Errors.Errors, validatedModel.fieldName.join("|") + "_" + validatedModel.errorCode);
        }
    }, {
        key: "_showBrowserError",
        value: function _showBrowserError(view) {
            var $wrapper = $(view).find("." + _CssClasses.CSS_CLASSES.WRAPPER);
            $wrapper.addClass(_CssClasses.CSS_CLASSES.WRAPPER_BROWSER_NOT_SUPPORTED);
            $wrapper.text(_Errors.Errors["not_supported_browser"] + window.DevicesUtils.getBrowserVersion());
        }
    }, {
        key: "_updatePreview",
        value: function _updatePreview(view, validatedModel) {
            var valid_model = validatedModel.value;
            var timerViewHandler = $(view).find("." + _CssClasses.CSS_CLASSES.TIMER);
            var defaultButtonViewHandler = $(view).find("." + _CssClasses.CSS_CLASSES.DEFAULT_RECORDING_PLAY_BUTTON);
            var $wrapperViewHandler = $(view).find("." + _CssClasses.CSS_CLASSES.WRAPPER);
            var intensityView = $(view).find("." + _CssClasses.CSS_CLASSES.SOUND_INTENSITY);
            var dottedSoundIntensityView = $(view).find("." + _CssClasses.CSS_CLASSES.DOTTED_SOUND_INTENSITY);
            var playButton = $(view).find("." + _CssClasses.CSS_CLASSES.PLAY_BUTTON);

            if (valid_model.extendedMode) {
                intensityView.css('display', 'none');
                playButton.css('display', 'none');
                dottedSoundIntensityView.css('display', '');
                defaultButtonViewHandler.hide();
                timerViewHandler.text('00:00');
                $wrapperViewHandler.addClass(_CssClasses.CSS_CLASSES.EXTENDED_MODE);
            } else {
                intensityView.css('display', '');
                dottedSoundIntensityView.css('display', 'none');

                if (valid_model.isShowedTimer == false) timerViewHandler.hide();else timerViewHandler.show();

                if (!valid_model.isShowedDefaultRecordingButton) defaultButtonViewHandler.hide();else defaultButtonViewHandler.show();

                if (valid_model.isDisabled) {
                    this.addonViewService = new _AddonViewService.AddonViewService($wrapperViewHandler);
                    this.addonViewService.deactivate();
                }
            }
        }
    }, {
        key: "_hideSelectedElements",
        value: function _hideSelectedElements() {
            if (this.model.isShowedTimer == false) this.viewHandlers.$timerView.hide();
            if (this.model.isShowedDefaultRecordingButton == false) this.viewHandlers.$defaultRecordingPlayButtonView.hide();
        }
    }, {
        key: "_isBrowserNotSupported",
        value: function _isBrowserNotSupported() {
            var browser = window.DevicesUtils.getBrowserVersion().split(" ")[0].toLowerCase();
            var browserVersion = window.DevicesUtils.getBrowserVersion().split(" ")[1];

            if (browser.indexOf("safari") &gt; -1 &amp;&amp; browserVersion &lt; 11) return true;

            if (browser.indexOf("chrome") &gt; -1 &amp;&amp; browserVersion &lt; 53) return true;

            if (window.DevicesUtils.isInternetExplorer()) return true;

            return false;
        }
    }, {
        key: "_loadWebViewMessageListener",
        value: function _loadWebViewMessageListener() {
            var _this4 = this;

            window.addEventListener('message', function (event) {
                try {
                    var eventData = JSON.parse(event.data);
                    var isTypePlatform = eventData.type ? eventData.type.toLowerCase() === 'platform' : false;
                    var isValueMlibro = eventData.value ? eventData.value.toLowerCase().includes('mlibro') : false;
                    if (isTypePlatform &amp;&amp; isValueMlibro) _this4._handleWebViewBehaviour();
                } catch (e) {
                    if (e instanceof SyntaxError) {
                        return;
                    }
                }
            }, false);
        }
    }, {
        key: "_handleWebViewBehaviour",
        value: function _handleWebViewBehaviour() {
            var _this5 = this;

            if (this.platform === undefined || this.platform === null) {
                this.platform = 'mlibro';
                window.addEventListener('message', function (event) {
                    var eventData = JSON.parse(event.data);
                    var isTypeRecording = eventData.type ? eventData.type.toLowerCase() === 'recording' : false;
                    var isTargetMe = eventData.target ? eventData.target === _this5.model.ID : false;
                    var isStateLoading = _this5.mediaState.isLoading();
                    if (isTypeRecording &amp;&amp; isTargetMe &amp;&amp; isStateLoading) {
                        _this5.addonState.setRecordingBase64(eventData.value);
                        _this5.player.reset();
                        _this5.player.setRecording(eventData.value);
                    } else {
                        console.log("The recording has not been received");
                    }
                }, false);
            }
        }
    }, {
        key: "_executeNotification",
        value: function _executeNotification(notifyInput) {
            try {
                if (mLibroChromium != undefined) {
                    mLibroChromium.notify(notifyInput);
                } else {
                    window.external.notify(notifyInput);
                }
            } catch (e) {
                // silent message
                // can't use a conditional expression
                // https://social.msdn.microsoft.com/Forums/en-US/1a8b3295-cd4d-4916-9cf6-666de1d3e26c/windowexternalnotify-always-undefined?forum=winappswithcsharp
            }
        }
    }, {
        key: "_handleMlibroStartRecording",
        value: function _handleMlibroStartRecording() {
            this.mediaState.setRecording();
            this.timer.reset();
            this.timer.startDecrementalCountdown(this.recordingTimeLimiter.maxTime);
            this.recordingTimeLimiter.startCountdown();
            this._executeNotification(JSON.stringify({ type: "mediaRecord", target: this.model.ID }));
        }
    }, {
        key: "_handleMlibroStopRecording",
        value: function _handleMlibroStopRecording() {
            this.mediaState.setLoading();
            this.timer.stopCountdown();
            this.recordingTimeLimiter.stopCountdown();
            this._executeNotification(JSON.stringify({ type: "mediaStop", target: this.model.ID }));
        }
    }, {
        key: "_setEnableState",
        value: function _setEnableState(isEnable) {
            if (isEnable) {
                this.addonState.setEnabled(true);
                this.activate();
            } else {
                this.addonState.setEnabled(false);
                this.deactivate();
            }
        }
    }, {
        key: "_upgradeModel",
        value: function _upgradeModel(model) {
            var upgradedModel = this._upgradeIsDisabled(model);
            upgradedModel = this._upgradeEnableInErrorCheckigMode(upgradedModel);
            upgradedModel = this._upgradeExtendedMode(upgradedModel);
            upgradedModel = this._upgradeResetDialog(upgradedModel);
            upgradedModel = this._upgradeDisableRecording(upgradedModel);
            upgradedModel = this._upgradeEnableIntensityChangeEvents(upgradedModel);
            upgradedModel = this._upgradeLangTag(upgradedModel);
            upgradedModel = this._upgradeSpeechTexts(upgradedModel);
            return upgradedModel;
        }
    }, {
        key: "_upgradeIsDisabled",
        value: function _upgradeIsDisabled(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["isDisabled"]) {
                upgradedModel["isDisabled"] = "False";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeEnableInErrorCheckigMode",
        value: function _upgradeEnableInErrorCheckigMode(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["enableInErrorCheckingMode"]) {
                upgradedModel["enableInErrorCheckingMode"] = "False";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeExtendedMode",
        value: function _upgradeExtendedMode(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["extendedMode"]) {
                upgradedModel["extendedMode"] = "False";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeResetDialog",
        value: function _upgradeResetDialog(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["resetDialogLabels"]) {
                upgradedModel["resetDialogLabels"] = {
                    "resetDialogText": { "resetDialogLabel": "" },
                    "resetDialogConfirm": { "resetDialogLabel": "" },
                    "resetDialogDeny": { "resetDialogLabel": "" }
                };
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeDisableRecording",
        value: function _upgradeDisableRecording(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["disableRecording"]) {
                upgradedModel["disableRecording"] = "False";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeEnableIntensityChangeEvents",
        value: function _upgradeEnableIntensityChangeEvents(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["enableIntensityChangeEvents"]) {
                upgradedModel["enableIntensityChangeEvents"] = "False";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeLangTag",
        value: function _upgradeLangTag(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["langAttribute"]) {
                upgradedModel["langAttribute"] = "";
            }

            return upgradedModel;
        }
    }, {
        key: "_upgradeSpeechTexts",
        value: function _upgradeSpeechTexts(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["speechTexts"]) {
                upgradedModel["speechTexts"] = {};
            }
            if (!upgradedModel["speechTexts"]["DefaultRecordingPlayButton"]) {
                upgradedModel["speechTexts"]["DefaultRecordingPlayButton"] = { DefaultRecordingPlayButton: "" };
            }
            if (!upgradedModel["speechTexts"]["RecordingButton"]) {
                upgradedModel["speechTexts"]["RecordingButton"] = { RecordingButton: "" };
            }
            if (!upgradedModel["speechTexts"]["PlayButton"]) {
                upgradedModel["speechTexts"]["PlayButton"] = { PlayButton: "" };
            }
            if (!upgradedModel["speechTexts"]["ResetButton"]) {
                upgradedModel["speechTexts"]["ResetButton"] = { ResetButton: "" };
            }
            if (!upgradedModel["speechTexts"]["DownloadButton"]) {
                upgradedModel["speechTexts"]["DownloadButton"] = { DownloadButton: "" };
            }
            if (!upgradedModel["speechTexts"]["ResetDialog"]) {
                upgradedModel["speechTexts"]["ResetDialog"] = { ResetDialog: "" };
            }
            if (!upgradedModel["speechTexts"]["StartRecording"]) {
                upgradedModel["speechTexts"]["StartRecording"] = { StartRecording: "" };
            }
            if (!upgradedModel["speechTexts"]["StopRecording"]) {
                upgradedModel["speechTexts"]["StopRecording"] = { StopRecording: "" };
            }
            if (!upgradedModel["speechTexts"]["Disabled"]) {
                upgradedModel["speechTexts"]["Disabled"] = { Disabled: "" };
            }

            return upgradedModel;
        }
    }, {
        key: "setWCAGStatus",
        value: function setWCAGStatus(isWCAGOn) {
            this.isWCAGOn = isWCAGOn;
        }
    }, {
        key: "_buildKeyboardController",
        value: function _buildKeyboardController() {
            var columnsCount = 1;
            var model = this.model;
            var mediaState = this.mediaState;
            var activationState = this.activationState;
            var speak = this._speak.bind(this);
            var speakAndExecuteCallback = this._speakAndExecuteCallback.bind(this);

            if (this.model.extendedMode) {
                this.keyboardControllerObject = new _ExtendedKeyboardController.ExtendedKeyboardController(this._getElementsForExtendedKeyboardNavigation(), columnsCount, model, mediaState, activationState, speak, speakAndExecuteCallback);
            } else {
                this.keyboardControllerObject = new _DefaultKeyboardController.DefaultKeyboardController(this._getElementsForDefaultKeyboardNavigation(), columnsCount, model, mediaState, activationState, speak, speakAndExecuteCallback);
            }
        }
    }, {
        key: "getKeyboardController",
        value: function getKeyboardController() {
            return this.keyboardControllerObject;
        }
    }, {
        key: "_getElementsForDefaultKeyboardNavigation",
        value: function _getElementsForDefaultKeyboardNavigation() {
            return $(this.view).find("\n            ." + _CssClasses.CSS_CLASSES.DEFAULT_RECORDING_PLAY_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.RECORDING_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.PLAY_BUTTON + "\n        ");
        }
    }, {
        key: "_getElementsForExtendedKeyboardNavigation",
        value: function _getElementsForExtendedKeyboardNavigation() {
            return $(this.view).find("\n            ." + _CssClasses.CSS_CLASSES.RECORDING_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.PLAY_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.RESET_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.DOWNLOAD_BUTTON + "\n        ");
        }
    }, {
        key: "_getElementsForResetDialogKeyboardNavigation",
        value: function _getElementsForResetDialogKeyboardNavigation() {
            return $(this.view).find("\n            ." + _CssClasses.CSS_CLASSES.DIALOG_TEXT + ",\n            ." + _CssClasses.CSS_CLASSES.CONFIRM_BUTTON + ",\n            ." + _CssClasses.CSS_CLASSES.DENY_BUTTON + "\n        ");
        }
    }, {
        key: "_speak",
        value: function _speak(data) {
            var tts = this.keyboardControllerObject.getTextToSpeechOrNull(this.playerController);
            if (tts &amp;&amp; this.isWCAGOn) {
                tts.speak(data);
            }
        }
    }, {
        key: "_speakAndExecuteCallback",
        value: function _speakAndExecuteCallback(data, callbackFunction) {
            var tts = this.keyboardControllerObject.getTextToSpeechOrNull(this.playerController);
            if (tts &amp;&amp; this.isWCAGOn) {
                tts.speakWithCallback(data, callbackFunction);
            } else {
                callbackFunction();
            }
        }
    }]);

    return MediaRecorder;
}();

/***/ }),
/* 18 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.validateModel = validateModel;

var _DefaultValues = __webpack_require__(19);

function validateModel(model) {
    var modelValidator = new ModelValidator();

    return modelValidator.validate(model, [ModelValidators.DumbString("ID"), ModelValidators.Boolean("Is Visible"), ModelValidators.Integer("maxTime", {
        minValue: 0,
        maxValue: _DefaultValues.DefaultValues.MAX_TIME,
        default: _DefaultValues.DefaultValues.DEFAULT_MAX_TIME
    }), ModelValidators.String("defaultRecording", {
        trim: true,
        default: ""
    }), ModelValidators.String("startRecordingSound", {
        trim: true,
        default: ""
    }), ModelValidators.String("stopRecordingSound", {
        trim: true,
        default: ""
    }), ModelValidators.Boolean("isResetRemovesRecording"), ModelValidators.Boolean("isShowedTimer"), ModelValidators.Boolean("isShowedDefaultRecordingButton"), ModelValidators.Boolean("enableInErrorCheckingMode"), ModelValidators.Boolean("isDisabled"), ModelValidators.Boolean("extendedMode"), ModelValidators.Boolean("disableRecording"), ModelValidators.Boolean("enableIntensityChangeEvents"), ModelValidators.StaticList('resetDialogLabels', {
        'resetDialogText': [ModelValidators.String('resetDialogLabel', { default: 'Are you sure you want to reset the recording?' })],
        'resetDialogConfirm': [ModelValidators.String('resetDialogLabel', { default: 'Yes' })],
        'resetDialogDeny': [ModelValidators.String('resetDialogLabel', { default: 'No' })]
    }), ModelValidators.String("langAttribute", {
        trim: true,
        default: ""
    })]);
}

/***/ }),
/* 19 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
var DefaultValues = exports.DefaultValues = {
    MAX_TIME: 30 * 60,
    DEFAULT_MAX_TIME: 10
};

/***/ }),
/* 20 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var ActivationState = exports.ActivationState = function () {
    function ActivationState() {
        _classCallCheck(this, ActivationState);

        this.values = {
            ACTIVE: 0,
            INACTIVE: 1
        };

        this._value = this.values.ACTIVE;
    }

    _createClass(ActivationState, [{
        key: "isActive",
        value: function isActive() {
            return this._value === this.values.ACTIVE;
        }
    }, {
        key: "isInactive",
        value: function isInactive() {
            return this._value === this.values.INACTIVE;
        }
    }, {
        key: "setActive",
        value: function setActive() {
            this._value = this.values.ACTIVE;
        }
    }, {
        key: "setInactive",
        value: function setInactive() {
            this._value = this.values.INACTIVE;
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.value = null;
            this.values = null;
        }
    }]);

    return ActivationState;
}();

/***/ }),
/* 21 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var MediaState = exports.MediaState = function () {
    function MediaState() {
        _classCallCheck(this, MediaState);

        this.values = {
            NEW: 0,
            BLOCKED: 1, // waiting for a resources permit
            RECORDING: 2,
            LOADING: 3,
            LOADED: 4,
            PLAYING: 5,
            PLAYING_DEFAULT_RECORDING: 6,
            LOADED_DEFAULT_RECORDING: 7,
            BLOCKED_SAFARI: 8
        };

        this._value = this.values.NEW;
    }

    _createClass(MediaState, [{
        key: "isNew",
        value: function isNew() {
            return this._value === this.values.NEW;
        }
    }, {
        key: "isRecording",
        value: function isRecording() {
            return this._value === this.values.RECORDING;
        }
    }, {
        key: "isLoading",
        value: function isLoading() {
            return this._value === this.values.LOADING;
        }
    }, {
        key: "isLoaded",
        value: function isLoaded() {
            return this._value === this.values.LOADED;
        }
    }, {
        key: "isPlaying",
        value: function isPlaying() {
            return this._value === this.values.PLAYING;
        }
    }, {
        key: "isPlayingDefaultRecording",
        value: function isPlayingDefaultRecording() {
            return this._value === this.values.PLAYING_DEFAULT_RECORDING;
        }
    }, {
        key: "isLoadedDefaultRecording",
        value: function isLoadedDefaultRecording() {
            return this._value === this.values.LOADED_DEFAULT_RECORDING;
        }
    }, {
        key: "isBlockedSafari",
        value: function isBlockedSafari() {
            return this._value === this.values.BLOCKED_SAFARI;
        }
    }, {
        key: "isBlocked",
        value: function isBlocked() {
            return this._value === this.values.BLOCKED;
        }
    }, {
        key: "setNew",
        value: function setNew() {
            this._value = this.values.NEW;
        }
    }, {
        key: "setRecording",
        value: function setRecording() {
            this._value = this.values.RECORDING;
        }
    }, {
        key: "setLoading",
        value: function setLoading() {
            this._value = this.values.LOADING;
        }
    }, {
        key: "setLoaded",
        value: function setLoaded() {
            this._value = this.values.LOADED;
        }
    }, {
        key: "setPlaying",
        value: function setPlaying() {
            this._value = this.values.PLAYING;
        }
    }, {
        key: "setPlayingDefaultRecording",
        value: function setPlayingDefaultRecording() {
            this._value = this.values.PLAYING_DEFAULT_RECORDING;
        }
    }, {
        key: "setLoadedDefaultRecording",
        value: function setLoadedDefaultRecording() {
            this._value = this.values.LOADED_DEFAULT_RECORDING;
        }
    }, {
        key: "setBlocked",
        value: function setBlocked() {
            this._value = this.values.BLOCKED;
        }
    }, {
        key: "setBlockedSafari",
        value: function setBlockedSafari() {
            this._value = this.values.BLOCKED_SAFARI;
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this._value = null;
            this.values = null;
        }
    }]);

    return MediaState;
}();

/***/ }),
/* 22 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
var Errors = exports.Errors = {
    "maxTime_INT02": "Time value contains non numerical characters",
    "maxTime_INT03": "Recording can not take more than 30 minutes",
    "maxTime_INT04": "Time in seconds cannot be negative value",
    "type_EV01": "Selected type is not supported",
    "not_supported_browser": "Your browser is not supported: ",
    "safari_select_recording_button_again": "Please click start recording button again. First time we tried to access your microphone. Now we will record it."
};

/***/ }),
/* 23 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.PlayButton = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _Button2 = __webpack_require__(1);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var PlayButton = exports.PlayButton = function (_Button) {
    _inherits(PlayButton, _Button);

    function PlayButton(_ref) {
        var $view = _ref.$view,
            state = _ref.state;

        _classCallCheck(this, PlayButton);

        var _this = _possibleConstructorReturn(this, (PlayButton.__proto__ || Object.getPrototypeOf(PlayButton)).call(this, $view));

        _this.state = state;
        return _this;
    }

    _createClass(PlayButton, [{
        key: "destroy",
        value: function destroy() {
            _get(PlayButton.prototype.__proto__ || Object.getPrototypeOf(PlayButton.prototype), "destroy", this).call(this);
            this.state = null;
        }
    }, {
        key: "_eventHandler",
        value: function _eventHandler() {
            if (this.state.isLoaded()) this._startPlaying();else if (this.state.isPlaying()) this._stopPlaying();
        }
    }, {
        key: "_startPlaying",
        value: function _startPlaying() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onStartPlayingCallback();
        }
    }, {
        key: "_stopPlaying",
        value: function _stopPlaying() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onStopPlayingCallback();
        }
    }, {
        key: "onStartPlaying",
        set: function set(callback) {
            this.onStartPlayingCallback = callback;
        }
    }, {
        key: "onStopPlaying",
        set: function set(callback) {
            this.onStopPlayingCallback = callback;
        }
    }]);

    return PlayButton;
}(_Button2.Button);

/***/ }),
/* 24 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.ResetButton = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _Button2 = __webpack_require__(1);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var ResetButton = exports.ResetButton = function (_Button) {
    _inherits(ResetButton, _Button);

    function ResetButton($view) {
        _classCallCheck(this, ResetButton);

        return _possibleConstructorReturn(this, (ResetButton.__proto__ || Object.getPrototypeOf(ResetButton)).call(this, $view));
    }

    _createClass(ResetButton, [{
        key: "_eventHandler",
        value: function _eventHandler() {
            if (this.onResetCallback != null) {
                this.onResetCallback();
            }
        }
    }, {
        key: "onReset",
        set: function set(callback) {
            this.onResetCallback = callback;
        }
    }]);

    return ResetButton;
}(_Button2.Button);

/***/ }),
/* 25 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.ResetDialog = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var ResetDialog = exports.ResetDialog = function () {
    function ResetDialog($view, resetDialogLabels) {
        _classCallCheck(this, ResetDialog);

        this.$view = $view;
        this.labels = {
            text: resetDialogLabels['resetDialogText']['resetDialogLabel'],
            confirm: resetDialogLabels['resetDialogConfirm']['resetDialogLabel'],
            deny: resetDialogLabels['resetDialogDeny']['resetDialogLabel']
        };
        this._createView();
    }

    _createClass(ResetDialog, [{
        key: 'open',
        value: function open() {
            this.$view.css('display', 'block');
            this.$view.css('left', '');
            this.$view.css('top', '');
        }
    }, {
        key: 'close',
        value: function close() {
            this.$view.css('display', 'none');
        }
    }, {
        key: '_createView',
        value: function _createView() {
            this.$view.find("." + _CssClasses.CSS_CLASSES.DIALOG_TEXT).text(this.labels.text);
            this.$view.find("." + _CssClasses.CSS_CLASSES.CONFIRM_BUTTON).text(this.labels.confirm);
            this.$view.find("." + _CssClasses.CSS_CLASSES.DENY_BUTTON).text(this.labels.deny);
            this.$view.draggable({});
            var self = this;
            this.$view.find("." + _CssClasses.CSS_CLASSES.CONFIRM_BUTTON).click(function () {
                self.close();
                if (self.onConfirmCallback) self.onConfirmCallback();
            });
            this.$view.find("." + _CssClasses.CSS_CLASSES.DENY_BUTTON).click(function () {
                self.close();
                if (self.onDenyCallback) self.onDenyCallback();
            });
        }
    }, {
        key: 'onConfirm',
        set: function set(callback) {
            this.onConfirmCallback = callback;
        }
    }, {
        key: 'onDeny',
        set: function set(callback) {
            this.onDenyCallback = callback;
        }
    }]);

    return ResetDialog;
}();

/***/ }),
/* 26 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.DownloadButton = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _Button2 = __webpack_require__(1);

var _BlobService = __webpack_require__(4);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var DownloadButton = exports.DownloadButton = function (_Button) {
    _inherits(DownloadButton, _Button);

    function DownloadButton(_ref) {
        var $view = _ref.$view,
            addonState = _ref.addonState;

        _classCallCheck(this, DownloadButton);

        var _this = _possibleConstructorReturn(this, (DownloadButton.__proto__ || Object.getPrototypeOf(DownloadButton)).call(this, $view));

        _this.addonState = addonState;
        return _this;
    }

    _createClass(DownloadButton, [{
        key: "_eventHandler",
        value: function _eventHandler() {
            if (this.addonState.recording) {
                this.downloadRecording();
            }
        }
    }, {
        key: "downloadRecording",
        value: function downloadRecording() {
            var element = document.createElement("a");
            element.setAttribute("id", "dl");
            element.setAttribute("download", "recording.mp3");
            element.setAttribute("href", "#");

            this.addonState.getMP3Blob().then(function (mp3Blob) {
                return _BlobService.BlobService.serialize(mp3Blob);
            }).then(function (b64Recording) {
                function handleDownloadRecording() {
                    var data = b64Recording;
                    data = data.replace(/^data:audio\/[^;]*/, 'data:application/octet-stream');
                    data = data.replace(/^data:application\/octet-stream/, 'data:application/octet-stream;headers=Content-Disposition%3A%20attachment%3B%20filename=recording.mp3');
                    this.href = data;
                }
                element.onclick = handleDownloadRecording;
                element.click();
            });
        }
    }]);

    return DownloadButton;
}(_Button2.Button);

/***/ }),
/* 27 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Timer = exports.Timer = function () {
    function Timer($view) {
        _classCallCheck(this, Timer);

        this.$view = $view;
        this.interval = null;
        this.currentMinutes = 0;
        this.currentSeconds = 0;
        this.loadedMinutes = 0;
        this.loadedSeconds = 0;
        this.duration = 0;
        this.isLoaded = false;

        this.$view.css("z-index", "100");
        this._updateText();
    }

    _createClass(Timer, [{
        key: "startCountdown",
        value: function startCountdown() {
            var _this = this;

            this._clearCurrentTime();
            this.interval = setInterval(function () {
                _this._incrementTimer();
                _this._updateText();
            }, 1000);
        }
    }, {
        key: "startDecrementalCountdown",
        value: function startDecrementalCountdown(duration) {
            var _this2 = this;

            this._clearCurrentTime();
            this.setDuration(duration);
            this.currentMinutes = this.loadedMinutes;
            this.currentSeconds = this.loadedSeconds;
            this._updateText();
            this.interval = setInterval(function () {
                _this2._decrementTimer();
                _this2._updateText();
            }, 1000);
        }
    }, {
        key: "stopCountdown",
        value: function stopCountdown() {
            if (this.interval != null) {
                clearInterval(this.interval);
                this.interval = null;
            }
            this._clearCurrentTime();
            this._updateText();
        }
    }, {
        key: "setDuration",
        value: function setDuration(duration) {
            this.duration = duration;
            this.loadedMinutes = parseInt(duration / 60);
            this.loadedSeconds = parseInt(duration % 60);
            this.isLoaded = true;
            this._updateText();
        }
    }, {
        key: "reset",
        value: function reset() {
            clearInterval(this.interval);
            this.isLoaded = false;
            this._clearCurrentTime();
            this._clearLoadedTime();
            this._updateText();
        }
    }, {
        key: "destroy",
        value: function destroy() {
            clearInterval(this.interval);
            this.interval = null;
            this.$view.remove();
            this.$view = null;
        }
    }, {
        key: "_clearCurrentTime",
        value: function _clearCurrentTime() {
            this.currentMinutes = 0;
            this.currentSeconds = 0;
        }
    }, {
        key: "_clearLoadedTime",
        value: function _clearLoadedTime() {
            this.loadedMinutes = 0;
            this.loadedSeconds = 0;
        }
    }, {
        key: "_incrementTimer",
        value: function _incrementTimer() {
            this.currentSeconds++;

            if (this.currentSeconds &gt;= 60) {
                this.currentSeconds = 0;
                this.currentMinutes++;
            }
        }
    }, {
        key: "_decrementTimer",
        value: function _decrementTimer() {
            this.currentSeconds--;

            if (this.currentSeconds &lt; 0) {
                this.currentSeconds = 59;
                this.currentMinutes--;
            }
        }
    }, {
        key: "_updateText",
        value: function _updateText() {
            this.$view[0].innerText = this.isLoaded ? this._generateTextTime(this.currentMinutes, this.currentSeconds) + " / " + this._generateTextTime(this.loadedMinutes, this.loadedSeconds) : this._generateTextTime(this.currentMinutes, this.currentSeconds);
        }
    }, {
        key: "_generateTextTime",
        value: function _generateTextTime(minutes, seconds) {
            var text = "";
            text += minutes &lt; 10 ? "0" + minutes : minutes;
            text += ":";
            text += seconds &lt; 10 ? "0" + seconds : seconds;

            return text;
        }
    }, {
        key: "setTime",
        value: function setTime(seconds) {
            this.currentMinutes = parseInt(seconds / 60);
            this.currentSeconds = parseInt(seconds % 60);
            this._updateText();
        }
    }]);

    return Timer;
}();

/***/ }),
/* 28 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.ProgressBar = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var ProgressBar = exports.ProgressBar = function () {
    function ProgressBar($view) {
        _classCallCheck(this, ProgressBar);

        this.$view = $view;
        this.progress = 0.0;
        this.$slider = $view.find("." + _CssClasses.CSS_CLASSES.PROGRESS_BAR_SLIDER);
        this.maxWidth = $view[0].offsetWidth - this.$slider[0].offsetWidth;
        var self = this;
        this.$slider.draggable({
            axis: "x",
            containment: "parent",
            start: function start(event, ui) {
                self._startDragging();
            },
            stop: function stop(event, ui) {
                self._stopDragging();
            }
        });
    }

    _createClass(ProgressBar, [{
        key: "setProgress",
        value: function setProgress(progress) {
            this.progress = progress;
            if (this.progress &gt; 1.0) this.progress = 1.0;
            if (this.progress &lt; 0.0) this.progress = 0.0;
            this._updateView();
        }
    }, {
        key: "reset",
        value: function reset() {
            this.setProgress(0.0);
        }
    }, {
        key: "_updateView",
        value: function _updateView() {
            this._updateMaxWidth();
            var left = Math.round(this.maxWidth * this.progress);
            this.$slider.css('left', left + 'px');
        }
    }, {
        key: "_updateMaxWidth",
        value: function _updateMaxWidth() {
            if (this.maxWidth === 0) {
                this.maxWidth = this.$view[0].offsetWidth - this.$slider[0].offsetWidth;
            }
        }
    }, {
        key: "_startDragging",
        value: function _startDragging() {
            if (this.onStartDraggingCallback) {
                this.onStartDraggingCallback();
            }
        }
    }, {
        key: "_stopDragging",
        value: function _stopDragging() {
            if (this.onStopDraggingCallback) {
                this._updateMaxWidth();
                this.progress = this.$slider[0].offsetLeft / this.maxWidth;
                this.onStopDraggingCallback(this.progress);
            }
        }
    }, {
        key: "onStartDragging",
        set: function set(callback) {
            this.onStartDraggingCallback = callback;
        }
    }, {
        key: "onStopDragging",
        set: function set(callback) {
            this.onStopDraggingCallback = callback;
        }
    }]);

    return ProgressBar;
}();

/***/ }),
/* 29 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AddonState = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _BlobService = __webpack_require__(4);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var AddonState = exports.AddonState = function () {
    function AddonState() {
        _classCallCheck(this, AddonState);

        this.recording = null;
        this.visibility = null;
        this.enabled = null;
        this.mp3ConvertHandler = null;
    }

    _createClass(AddonState, [{
        key: "setMP3ConvertHandler",
        value: function setMP3ConvertHandler(handler) {
            this.mp3ConvertHandler = handler;
        }
    }, {
        key: "setRecordingBlob",
        value: function setRecordingBlob(blob) {
            var _this = this;

            _BlobService.BlobService.serialize(blob).then(function (recording) {
                return _this.recording = recording;
            });
        }
    }, {
        key: "setRecordingBase64",
        value: function setRecordingBase64(recording) {
            this.recording = recording;
        }
    }, {
        key: "getRecordingBlob",
        value: function getRecordingBlob() {
            var _this2 = this;

            return new Promise(function (resolve) {
                if (_this2.recording) resolve(_BlobService.BlobService.deserialize(_this2.recording));
            });
        }
    }, {
        key: "isEmpty",
        value: function isEmpty() {
            return this.recording == null;
        }
    }, {
        key: "isMP3Format",
        value: function isMP3Format(blob) {
            return !this.isEmpty() &amp;&amp; blob.type.includes("audio/mpeg");
        }
    }, {
        key: "isWAVFormat",
        value: function isWAVFormat(blob) {
            return !this.isEmpty() &amp;&amp; blob.type.includes("audio/wav");
        }
    }, {
        key: "getMP3File",
        value: function getMP3File() {
            return this.getMP3Blob().then(function (mp3Blob) {
                return new File([mp3Blob], "recording.mp3");
            });
        }
    }, {
        key: "getMP3Blob",
        value: function getMP3Blob() {
            var _this3 = this;

            return this.getRecordingBlob().then(function (blob) {
                if (_this3.isMP3Format(blob)) {
                    return blob;
                }
                if (!!_this3.mp3ConvertHandler &amp;&amp; _this3.mp3ConvertHandler.isSupported() &amp;&amp; _this3.mp3ConvertHandler.isValid &amp;&amp; !_this3.mp3ConvertHandler.isWorkerExist()) {
                    return _this3.convertWavBlobToMP3BlobByWorker(blob);
                }
                return _this3.convertWavBlobToMP3Blob(blob);
            });
        }
    }, {
        key: "convertWavBlobToMP3BlobByWorker",
        value: function convertWavBlobToMP3BlobByWorker(wavBlob) {
            var _this4 = this;

            return this.decodeVawBlobData(wavBlob).then(function (decodedData) {
                return _BlobService.BlobService.getMp3BlobFromDecodedDataByWorker(_this4.mp3ConvertHandler, decodedData);
            });
        }
    }, {
        key: "convertWavBlobToMP3Blob",
        value: function convertWavBlobToMP3Blob(wavBlob) {
            return this.decodeVawBlobData(wavBlob).then(function (decodedData) {
                return _BlobService.BlobService.getMp3BlobFromDecodedData(decodedData);
            });
        }
    }, {
        key: "decodeVawBlobData",
        value: function decodeVawBlobData(wavBlob) {
            var _this5 = this;

            return new Promise(function (resolve) {
                resolve(wavBlob);
            }).then(function (wavBlob) {
                File.prototype.arrayBuffer = File.prototype.arrayBuffer || _this5._fixArrayBuffer;
                Blob.prototype.arrayBuffer = Blob.prototype.arrayBuffer || _this5._fixArrayBuffer;

                return wavBlob.arrayBuffer();
            }).then(function (arrayBuffer) {
                window.AudioContext = window.AudioContext || window.webkitAudioContext;
                var context = new AudioContext();
                return new Promise(function (resolve) {
                    context.decodeAudioData(arrayBuffer, function (buffer) {
                        resolve(buffer);
                    });
                });
            });
        }

        //for some reason there is a bug in some lower Safari versions &lt;14, it cause arrayBuffer() undefined
        //https://gist.github.com/hanayashiki/8dac237671343e7f0b15de617b0051bd

    }, {
        key: "_fixArrayBuffer",
        value: function _fixArrayBuffer() {
            var _this6 = this;

            return new Promise(function (resolve) {
                var fr = new FileReader();
                fr.onload = function () {
                    resolve(fr.result);
                };
                fr.readAsArrayBuffer(_this6);
            });
        }
    }, {
        key: "setVisibility",
        value: function setVisibility(isVisible) {
            this.visibility = isVisible ? true : false;
        }
    }, {
        key: "getVisibility",
        value: function getVisibility() {
            var self = this;
            return new Promise(function (resolve) {
                if (self.visibility != null) resolve(self.visibility);
            });
        }
    }, {
        key: "setEnabled",
        value: function setEnabled(isEnable) {
            this.enabled = isEnable ? true : false;
        }
    }, {
        key: "getEnabled",
        value: function getEnabled() {
            var self = this;
            return new Promise(function (resolve) {
                if (self.enabled != null) resolve(self.enabled);
            });
        }
    }, {
        key: "getState",
        value: function getState() {
            var state = {
                recording: this.recording,
                visibility: this.visibility,
                enabled: this.enabled
            };
            return JSON.stringify(state);
        }
    }, {
        key: "reset",
        value: function reset() {
            this.recording = null;
            this.visibility = null;
            this.enabled = null;
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.recording = null;
            this.visibility = null;
            this.enabled = null;
            this.mp3ConvertHandler = null;
        }
    }]);

    return AddonState;
}();

/***/ }),
/* 30 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var RecordingTimeLimiter = exports.RecordingTimeLimiter = function () {
    function RecordingTimeLimiter(maxTime) {
        _classCallCheck(this, RecordingTimeLimiter);

        this.workingDelay = 0.2;
        this.maxTime = maxTime + this.workingDelay;
        this.counter = 0;
        this.interval;
        this.callback;
    }

    _createClass(RecordingTimeLimiter, [{
        key: "startCountdown",
        value: function startCountdown() {
            var _this = this;

            if (this.maxTime || this.callback) this.interval = setInterval(function () {
                return _this._incrementTimer();
            }, this.workingDelay * 1000);
        }
    }, {
        key: "stopCountdown",
        value: function stopCountdown() {
            clearInterval(this.interval);
            this.counter = 0;
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.callback = function () {};
            clearInterval(this.interval);
            this.interval = null;
            this.callback = null;
        }
    }, {
        key: "_incrementTimer",
        value: function _incrementTimer() {
            this.counter += this.workingDelay;
            if (this.counter &gt;= this.maxTime) {
                this.stopCountdown();
                this.callback();
            }
        }
    }, {
        key: "onTimeExpired",
        set: function set(callback) {
            this.callback = callback;
        }
    }]);

    return RecordingTimeLimiter;
}();

/***/ }),
/* 31 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.DottedSoundIntensity = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _SoundIntensity2 = __webpack_require__(5);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var DottedSoundIntensity = exports.DottedSoundIntensity = function (_SoundIntensity) {
    _inherits(DottedSoundIntensity, _SoundIntensity);

    function DottedSoundIntensity() {
        _classCallCheck(this, DottedSoundIntensity);

        return _possibleConstructorReturn(this, (DottedSoundIntensity.__proto__ || Object.getPrototypeOf(DottedSoundIntensity)).apply(this, arguments));
    }

    _createClass(DottedSoundIntensity, [{
        key: "_setIntensity",
        value: function _setIntensity(intensity) {
            this._clearIntensity();
            var cappedIntensity = intensity;
            if (cappedIntensity &gt; this.volumeLevels) cappedIntensity = this.volumeLevels;
            var heightPercent = cappedIntensity / this.volumeLevels;
            var heightDiff = heightPercent * (this.$view[0].offsetHeight - 6);
            var tallDotNewHeight = Math.round(6 + heightDiff);
            var shortDotNewHeight = Math.round(6 + heightDiff / 2);
            this.$view.find("." + _CssClasses.CSS_CLASSES.TALL_DOT).css('height', tallDotNewHeight + 'px');
            this.$view.find("." + _CssClasses.CSS_CLASSES.SHORT_DOT).css('height', shortDotNewHeight + 'px');
        }
    }, {
        key: "_clearIntensity",
        value: function _clearIntensity() {
            this.$view.find("." + _CssClasses.CSS_CLASSES.SOUND_INTENSITY_DOT).css('height', '');
        }
    }]);

    return DottedSoundIntensity;
}(_SoundIntensity2.SoundIntensity);

/***/ }),
/* 32 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.MediaAnalyserService = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _AnalyserProvider = __webpack_require__(33);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var MediaAnalyserService = exports.MediaAnalyserService = function () {
    function MediaAnalyserService() {
        _classCallCheck(this, MediaAnalyserService);

        this.audioContext = AudioContextSingleton.getOrCreate();
        this.mediaStreamSource = null;
        this.mediaElementSource = null;
    }

    _createClass(MediaAnalyserService, [{
        key: "createAnalyserFromStream",
        value: function createAnalyserFromStream(stream) {
            var _this = this;

            return new Promise(function (resolve) {
                // on Chrome when user hasn't interacted with the page before AudioContext was created it will be created in suspended state
                // this will happen if MediaRecorder is on the first page user visits (constructor call will happen before user interaction)
                // resume is then needed to unblock AudioContext (see https://goo.gl/7K7WLu)
                _this.audioContext.resume().then(function () {
                    _this.mediaStreamSource = _this.audioContext.createMediaStreamSource(stream);

                    var analyser = _AnalyserProvider.AnalyserProvider.create(_this.audioContext);
                    _this.mediaStreamSource.connect(analyser);

                    resolve(analyser);
                });
            });
        }
    }, {
        key: "createAnalyserFromElement",
        value: function createAnalyserFromElement(htmlMediaElement) {
            var _this2 = this;

            return new Promise(function (resolve) {
                // on Chrome when user hasn't interacted with the page before AudioContext was created it will be created in suspended state
                // this will happen if MediaRecorder is on the first page user visits (constructor call will happen before user interaction)
                // resume is then needed to unblock AudioContext (see https://goo.gl/7K7WLu)
                _this2.audioContext.resume().then(function () {
                    if (!_this2.mediaElementSource) _this2.mediaElementSource = _this2.audioContext.createMediaElementSource(htmlMediaElement);

                    var analyser = _AnalyserProvider.AnalyserProvider.create(_this2.audioContext);
                    _this2.mediaElementSource.connect(analyser);
                    analyser.connect(_this2.audioContext.destination);

                    resolve(analyser);
                });
            });
        }
    }, {
        key: "closeAnalyzing",
        value: function closeAnalyzing() {
            if (this.mediaStreamSource) this.mediaStreamSource.disconnect();
            if (this.mediaElementSource) this.mediaElementSource.disconnect();
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.closeAnalyzing();
            AudioContextSingleton.close();
            this.audioContext = null;
            this.mediaElementSource = null;
            this.mediaStreamSource = null;
        }
    }]);

    return MediaAnalyserService;
}();

/***/ }),
/* 33 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var AnalyserProvider = exports.AnalyserProvider = function () {
    function AnalyserProvider() {
        _classCallCheck(this, AnalyserProvider);
    }

    _createClass(AnalyserProvider, null, [{
        key: "create",
        value: function create(audioContext) {
            return this._createAnalyser(audioContext);
        }
    }, {
        key: "_createAnalyser",
        value: function _createAnalyser(audioContext) {
            var analyser = audioContext.createAnalyser();
            analyser.fftSize = 1024;
            analyser.smoothingTimeConstant = 0.3;
            return analyser;
        }
    }]);

    return AnalyserProvider;
}();

/***/ }),
/* 34 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AudioLoader = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _Loader2 = __webpack_require__(35);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var AudioLoader = exports.AudioLoader = function (_Loader) {
    _inherits(AudioLoader, _Loader);

    function AudioLoader($view) {
        _classCallCheck(this, AudioLoader);

        return _possibleConstructorReturn(this, (AudioLoader.__proto__ || Object.getPrototypeOf(AudioLoader)).call(this, $view));
    }

    _createClass(AudioLoader, [{
        key: "show",
        value: function show() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.AUDIO_LOADER);
        }
    }, {
        key: "hide",
        value: function hide() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.AUDIO_LOADER);
        }
    }]);

    return AudioLoader;
}(_Loader2.Loader);

/***/ }),
/* 35 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Loader = exports.Loader = function () {
    function Loader($view) {
        _classCallCheck(this, Loader);

        if (this.constructor === Loader) throw new Error("Cannot create an instance of Loader abstract class");

        this.$view = $view;
    }

    _createClass(Loader, [{
        key: "show",
        value: function show() {
            throw new Error("Show method is not implemented");
        }
    }, {
        key: "hide",
        value: function hide() {
            throw new Error("Hide method is not implemented");
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.$view.remove();
            this.$view = null;
        }
    }]);

    return Loader;
}();

/***/ }),
/* 36 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var SoundEffect = exports.SoundEffect = function () {
    function SoundEffect(sound, $wrapper) {
        _classCallCheck(this, SoundEffect);

        this.sound = sound;
        this.$wrapper = $wrapper;
        this.audioNode = document.createElement("audio");
        this.audioNode.src = sound;
        this.audioNode.style.display = "none";
        this.$wrapper.append(this.audioNode);
        this.startCallback = function () {};
        this.stopCallback = function () {};
    }

    _createClass(SoundEffect, [{
        key: "isValid",
        value: function isValid() {
            return this.sound != "" &amp;&amp; this.sound != null &amp;&amp; typeof this.sound != "undefined";
        }
    }, {
        key: "playSound",
        value: function playSound() {
            this.startCallback();
            var playPromise = this.audioNode.play();
            if (playPromise !== undefined) {
                playPromise.catch(function (error) {
                    console.log(error);
                });
            }
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.audioNode.pause();
            this.audioNode.src = "";
            this.audioNode.remove();
            this.audioNode = null;
            this.$wrapper.remove();
            this.$wrapper = null;
        }
    }, {
        key: "isBrowserRequiredReloadNode",
        value: function isBrowserRequiredReloadNode() {
            var navU = window.navigator.userAgent;
            return navU.indexOf('Android') &gt; -1 &amp;&amp; navU.indexOf('Mozilla/5.0') &gt; -1 &amp;&amp; navU.indexOf('AppleWebKit') &gt; -1;
        }
    }, {
        key: "_reloadAudioNode",
        value: function _reloadAudioNode() {
            this.audioNode.remove();
            this.audioNode = document.createElement("audio");
            this.audioNode.src = this.sound;
            this.audioNode.style.display = "none";
            this.$wrapper.append(this.audioNode);
            this.onStopCallback = this.stopCallback;
        }
    }, {
        key: "onStartCallback",
        set: function set(callback) {
            this.startCallback = callback;
        }
    }, {
        key: "onStopCallback",
        set: function set(callback) {
            var _this = this;

            this.stopCallback = callback;
            this.audioNode.onended = function () {
                callback();
                if (_this.isBrowserRequiredReloadNode()) _this._reloadAudioNode();
            };
        }
    }]);

    return SoundEffect;
}();

/***/ }),
/* 37 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.RecordButtonSoundEffect = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _RecordButton2 = __webpack_require__(3);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var RecordButtonSoundEffect = exports.RecordButtonSoundEffect = function (_RecordButton) {
    _inherits(RecordButtonSoundEffect, _RecordButton);

    function RecordButtonSoundEffect(recordButton, startRecordingSoundEffect, stopRecordingSoundEffect) {
        _classCallCheck(this, RecordButtonSoundEffect);

        var _this = _possibleConstructorReturn(this, (RecordButtonSoundEffect.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect)).call(this, { $view: recordButton.$view, state: recordButton.state }));

        _this.startRecordingSoundEffect = startRecordingSoundEffect;
        _this.stopRecordingSoundEffect = stopRecordingSoundEffect;
        return _this;
    }

    _createClass(RecordButtonSoundEffect, [{
        key: "_startRecording",
        value: function _startRecording() {
            if (this.startRecordingSoundEffect.isValid()) {
                this._recordWithSoundEffect();
            } else if (this._isKeyboardControllerNavigationActive()) {
                this._recordWithTTS();
            } else {
                _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_startRecording", this).call(this);
            }
        }
    }, {
        key: "_recordWithSoundEffect",
        value: function _recordWithSoundEffect() {
            this.startRecordingSoundEffect.onStartCallback = function () {};
            this.startRecordingSoundEffect.onStopCallback = function () {};
            _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_startRecording", this).call(this);
            this._playStartRecordingSoundEffect();

            if (this._isKeyboardControllerNavigationActive()) {
                this._keyboardController.onStartRecordingWhenSoundEffect();
            }
        }
    }, {
        key: "_playStartRecordingSoundEffect",
        value: function _playStartRecordingSoundEffect() {
            var _this2 = this;

            if (this.startRecordingSoundEffect.isBrowserRequiredReloadNode()) setTimeout(function () {
                return _this2.startRecordingSoundEffect.playSound();
            }, 1000);else this.startRecordingSoundEffect.playSound();
        }
    }, {
        key: "_recordWithTTS",
        value: function _recordWithTTS() {
            var callbackFunction = _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_startRecording", this).bind(this);
            this._keyboardController.onStartRecording(callbackFunction);
        }
    }, {
        key: "_stopRecording",
        value: function _stopRecording() {
            if (this._keyboardController.isCurrentElementDisabled()) return;

            if (this.stopRecordingSoundEffect.isValid()) {
                this._onStopRecordingWithSoundEffect();
            } else if (this._isKeyboardControllerNavigationActive()) {
                this._onStopRecordingWithTTS();
            } else {
                _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_stopRecording", this).call(this);
            }
        }
    }, {
        key: "_onStopRecordingWithSoundEffect",
        value: function _onStopRecordingWithSoundEffect() {
            var _this3 = this;

            this.stopRecordingSoundEffect.onStartCallback = function () {
                _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_stopRecording", _this3).call(_this3);
                _this3.deactivate();
            };
            this.stopRecordingSoundEffect.onStopCallback = function () {
                _this3.activate();
            };
            this.stopRecordingSoundEffect.playSound();

            if (this._isKeyboardControllerNavigationActive()) {
                this._keyboardController.onStopRecordingWhenSoundEffect();
            }
        }
    }, {
        key: "_onStopRecordingWithTTS",
        value: function _onStopRecordingWithTTS() {
            _get(RecordButtonSoundEffect.prototype.__proto__ || Object.getPrototypeOf(RecordButtonSoundEffect.prototype), "_stopRecording", this).call(this);
            this._keyboardController.onStopRecording();
        }
    }, {
        key: "_isKeyboardControllerNavigationActive",
        value: function _isKeyboardControllerNavigationActive() {
            return this._keyboardController.keyboardNavigationActive === true;
        }
    }]);

    return RecordButtonSoundEffect;
}(_RecordButton2.RecordButton);

/***/ }),
/* 38 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AddonViewService = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var AddonViewService = exports.AddonViewService = function () {
    function AddonViewService($wrapperView) {
        _classCallCheck(this, AddonViewService);

        this.$view = $wrapperView;
    }

    _createClass(AddonViewService, [{
        key: 'setVisibility',
        value: function setVisibility(isVisible) {
            this.$view.css('visibility', isVisible ? 'visible' : 'hidden');
            this.$view.parent().css('visibility', isVisible ? 'visible' : 'hidden');
        }
    }, {
        key: 'activate',
        value: function activate() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.DISABLED);
        }
    }, {
        key: 'deactivate',
        value: function deactivate() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.DISABLED);
        }
    }, {
        key: 'destroy',
        value: function destroy() {
            this.$view = null;
        }
    }]);

    return AddonViewService;
}();

/***/ }),
/* 39 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AudioResourcesProvider = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _ResourcesProvider2 = __webpack_require__(40);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var AudioResourcesProvider = exports.AudioResourcesProvider = function (_ResourcesProvider) {
    _inherits(AudioResourcesProvider, _ResourcesProvider);

    function AudioResourcesProvider() {
        _classCallCheck(this, AudioResourcesProvider);

        return _possibleConstructorReturn(this, (AudioResourcesProvider.__proto__ || Object.getPrototypeOf(AudioResourcesProvider)).apply(this, arguments));
    }

    _createClass(AudioResourcesProvider, [{
        key: "_getOptions",
        value: function _getOptions() {
            return {
                audio: DevicesUtils.isEdge() ? true : {
                    echoCancellation: false
                }
            };
        }
    }]);

    return AudioResourcesProvider;
}(_ResourcesProvider2.ResourcesProvider);

/***/ }),
/* 40 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var ResourcesProvider = exports.ResourcesProvider = function () {
    function ResourcesProvider($view) {
        _classCallCheck(this, ResourcesProvider);

        this.RESOURCES_ERROR_MESSAGE = "Multimedia resources not available";

        if (this.constructor === ResourcesProvider) throw new Error("Cannot create an instance of ResourcesProvider abstract class");

        this.$view = $view;
        this.stream = null;
    }

    _createClass(ResourcesProvider, [{
        key: "getMediaResources",
        value: function getMediaResources() {
            var _this = this;

            return new Promise(function (resolve) {
                navigator.mediaDevices.getUserMedia(_this._getOptions()).then(function (stream) {
                    _this.stream = stream;
                    resolve(stream);
                }).catch(function (error) {
                    console.error(error);
                    DOMOperationsUtils.showErrorMessage(_this.$view, [_this.RESOURCES_ERROR_MESSAGE], "0");
                });
            });
        }
    }, {
        key: "getStream",
        value: function getStream() {
            return this.stream;
        }
    }, {
        key: "destroy",
        value: function destroy() {
            if (this.stream) {
                this.stream.stop();
                this.stream = null;
            }
        }
    }, {
        key: "_getOptions",
        value: function _getOptions() {
            throw new Error("GetOptions accessor is not implemented");
        }
    }]);

    return ResourcesProvider;
}();

/***/ }),
/* 41 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AudioRecorder = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _BaseRecorder2 = __webpack_require__(42);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var AudioRecorder = exports.AudioRecorder = function (_BaseRecorder) {
    _inherits(AudioRecorder, _BaseRecorder);

    function AudioRecorder() {
        _classCallCheck(this, AudioRecorder);

        return _possibleConstructorReturn(this, (AudioRecorder.__proto__ || Object.getPrototypeOf(AudioRecorder)).apply(this, arguments));
    }

    _createClass(AudioRecorder, [{
        key: '_getOptions',
        value: function _getOptions() {
            var isEdge = DevicesUtils.isEdge();

            return {
                type: 'audio',
                mimeType: 'audio/wav',
                numberOfAudioChannels: 1,
                checkForInactiveTracks: true,
                bufferSize: 16384,
                disableLogs: true,
                recorderType: RecordRTC.StereoAudioRecorder,
                desiredSampRate: 22050
            };
        }
    }]);

    return AudioRecorder;
}(_BaseRecorder2.BaseRecorder);

/***/ }),
/* 42 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.BaseRecorder = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _Recorder2 = __webpack_require__(43);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var BaseRecorder = exports.BaseRecorder = function (_Recorder) {
    _inherits(BaseRecorder, _Recorder);

    function BaseRecorder() {
        _classCallCheck(this, BaseRecorder);

        var _this = _possibleConstructorReturn(this, (BaseRecorder.__proto__ || Object.getPrototypeOf(BaseRecorder)).call(this));

        if (_this.constructor === BaseRecorder) throw new Error("Cannot create an instance of Recorder abstract class");

        _this.recorder = null;
        _this.eventBus = null;
        _this.sourceID = '';
        return _this;
    }

    _createClass(BaseRecorder, [{
        key: "startRecording",
        value: function startRecording(stream) {
            var _this2 = this;

            this._clearRecorder();
            var audioContext = AudioContextSingleton.getOrCreate();
            this.recorder = RecordRTC(stream, this._getOptions());
            audioContext.resume().then(function () {
                _this2.recorder.startRecording();
                _this2._onStartRecordingCallback();
            });
        }
    }, {
        key: "stopRecording",
        value: function stopRecording() {
            var self = this;
            var promise = new Promise(function (resolve) {
                return self.recorder.stopRecording(function () {
                    return resolve(self.recorder.getBlob());
                });
            });
            promise.then(function () {
                return self._onStopRecordingCallback(self);
            });

            return promise;
        }
    }, {
        key: "setEventBus",
        value: function setEventBus(eventBus, sourceID) {
            this.eventBus = eventBus;
            this.sourceID = sourceID;
        }
    }, {
        key: "sendValueChangedEmptyEvent",
        value: function sendValueChangedEmptyEvent() {
            this._sendValueChangedEventCallback(this, 'empty');
        }
    }, {
        key: "sendPreDestroyedEmptyEvent",
        value: function sendPreDestroyedEmptyEvent() {
            if (this.eventBus) {
                var eventData = {
                    'source': this.sourceID,
                    'item': 'recorder',
                    'value': 'empty'
                };
                this.eventBus.sendEvent('PreDestroyed', eventData);
            }
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this.onAvailableRecordingCallback = function (blob) {};

            if (this.recorder) {
                this.recorder.stopRecording();
                this._clearRecorder();
            }
        }
    }, {
        key: "_clearRecorder",
        value: function _clearRecorder() {
            if (this.recorder) {
                this.recorder.destroy();
                this.recorder = null;
            }
        }
    }, {
        key: "_onStartRecordingCallback",
        value: function _onStartRecordingCallback() {
            this._sendValueChangedEventCallback(this, 'start');
        }
    }, {
        key: "_onStopRecordingCallback",
        value: function _onStopRecordingCallback(self) {
            self._sendValueChangedEventCallback(self, 'stop');
        }
    }, {
        key: "_sendValueChangedEventCallback",
        value: function _sendValueChangedEventCallback(self, value) {
            if (self.eventBus) {
                var eventData = {
                    'source': self.sourceID,
                    'item': 'recorder',
                    'value': value,
                    'score': ''
                };
                self.eventBus.sendEvent('ValueChanged', eventData);
            }
        }
    }, {
        key: "_getOptions",
        value: function _getOptions() {
            throw new Error("GetOptions accessor is not implemented");
        }
    }]);

    return BaseRecorder;
}(_Recorder2.Recorder);

/***/ }),
/* 43 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Recorder = exports.Recorder = function () {
    function Recorder() {
        _classCallCheck(this, Recorder);

        if (this.constructor === Recorder) throw new Error("Cannot create an instance of Recorder abstract class");
    }

    _createClass(Recorder, [{
        key: "startRecording",
        value: function startRecording(stream) {
            throw new Error("StartRecording method is not implemented");
        }
    }, {
        key: "stopRecording",
        value: function stopRecording() {
            throw new Error("StopRecording method is not implemented");
        }
    }, {
        key: "setEventBus",
        value: function setEventBus(eventBus, sourceID) {
            throw new Error("setEventBus method is not implemented");
        }
    }, {
        key: "destroy",
        value: function destroy() {
            throw new Error("Destroy method is not implemented");
        }
    }]);

    return Recorder;
}();

/***/ }),
/* 44 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AudioPlayer = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _BasePlayer2 = __webpack_require__(45);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var AudioPlayer = exports.AudioPlayer = function (_BasePlayer) {
    _inherits(AudioPlayer, _BasePlayer);

    function AudioPlayer($view, isMlibro) {
        _classCallCheck(this, AudioPlayer);

        var _this = _possibleConstructorReturn(this, (AudioPlayer.__proto__ || Object.getPrototypeOf(AudioPlayer)).call(this, $view, isMlibro));

        _this.mediaNode.style.display = "hidden";
        return _this;
    }

    _createClass(AudioPlayer, [{
        key: "_createMediaNode",
        value: function _createMediaNode() {
            return document.createElement("audio");
        }
    }]);

    return AudioPlayer;
}(_BasePlayer2.BasePlayer);

/***/ }),
/* 45 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.BasePlayer = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _Player2 = __webpack_require__(46);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var BasePlayer = exports.BasePlayer = function (_Player) {
    _inherits(BasePlayer, _Player);

    function BasePlayer($view, isMlibro) {
        _classCallCheck(this, BasePlayer);

        var _this = _possibleConstructorReturn(this, (BasePlayer.__proto__ || Object.getPrototypeOf(BasePlayer)).call(this));

        if (_this.constructor === BasePlayer) throw new Error("Cannot create an instance of BasePlayer abstract class");
        _this.isMlibro = isMlibro;
        _this.$view = $view;
        _this.hasRecording = false;
        _this.duration = null;
        _this.mediaNode = _this._createMediaNode();
        _this.mediaNode.controls = false;
        _this.$view.append(_this.mediaNode);
        _this.eventBus = null;
        _this.sourceID = '';
        _this.item = '';

        _this._enableEventsHandling();
        return _this;
    }

    _createClass(BasePlayer, [{
        key: "setRecording",
        value: function setRecording(source) {
            var _this2 = this;

            this.mediaNode.src = source;
            this.hasRecording = true;
            this._getDuration().then(function (duration) {
                _this2.onDurationChangeCallback(duration);
                _this2.duration = duration;
            }).catch(function (e) {
                _this2.hasRecording = false;
            });
        }
    }, {
        key: "startPlaying",
        value: function startPlaying() {
            var _this3 = this;

            return new Promise(function (resolve) {
                _this3.mediaNode.muted = false;
                if (_this3.onTimeUpdateCallback) {
                    _this3._enableTimerEventsHandling();
                }
                if (_this3._isNotOnlineResources(_this3.mediaNode.src)) resolve(_this3.mediaNode);
                _this3.mediaNode.play();
            });
        }
    }, {
        key: "stopPlaying",
        value: function stopPlaying() {
            var _this4 = this;

            return new Promise(function (resolve) {
                _this4.mediaNode.pause();
                _this4.mediaNode.currentTime = 0;
                if (_this4.onTimeUpdateCallback) {
                    _this4._disableTimerEventsHandling();
                }
                resolve();
            });
        }
    }, {
        key: "_enableTimerEventsHandling",
        value: function _enableTimerEventsHandling() {
            this.mediaNode.addEventListener('timeupdate', this.onTimeUpdateCallback);
            this.mediaNode.addEventListener('ended', this.onTimeUpdateCallback);
        }
    }, {
        key: "_disableTimerEventsHandling",
        value: function _disableTimerEventsHandling() {
            this.mediaNode.removeEventListener('timeupdate', this.onTimeUpdateCallback);
            this.mediaNode.removeEventListener('ended', this.onTimeUpdateCallback);
        }
    }, {
        key: "pausePlaying",
        value: function pausePlaying() {
            var _this5 = this;

            return new Promise(function (resolve) {
                _this5.mediaNode.pause();
                if (_this5.onTimeUpdateCallback) {
                    _this5.mediaNode.removeEventListener('timeupdate', _this5.onTimeUpdateCallback);
                    _this5.mediaNode.removeEventListener('ended', _this5.onTimeUpdateCallback);
                }
                resolve();
            });
        }
    }, {
        key: "setProgress",
        value: function setProgress(progress) {
            var _this6 = this;

            return new Promise(function (resolve) {
                _this6.mediaNode.currentTime = Math.round(_this6.duration * progress);
                resolve();
            });
        }
    }, {
        key: "reset",
        value: function reset() {
            this._disableEventsHandling();
            this.mediaNode.src = "";
            this.mediaNode.remove();
            this.hasRecording = false;
            this.duration = null;
            this.mediaNode = this._createMediaNode();
            this.mediaNode.controls = false;
            this.$view.append(this.mediaNode);
            this._enableEventsHandling();
        }
    }, {
        key: "destroy",
        value: function destroy() {
            this._disableEventsHandling();
            this.stopPlaying();
            this.mediaNode.src = "";
            this.mediaNode.remove();
            this.hasRecording = null;
            this.duration = null;
            this.$view.remove();
            this.mediaNode = null;
            this.$view = null;
        }
    }, {
        key: "setEventBus",
        value: function setEventBus(eventBus, sourceID, item) {
            this.eventBus = eventBus;
            this.sourceID = sourceID;
            this.item = item;
        }
    }, {
        key: "getCurrentTime",
        value: function getCurrentTime() {
            return this.mediaNode.currentTime;
        }
    }, {
        key: "setIsMlibro",
        value: function setIsMlibro(isMlibro) {
            this.isMlibro = isMlibro;
        }
    }, {
        key: "_enableEventsHandling",
        value: function _enableEventsHandling() {
            var _this7 = this;

            this.mediaNode.onloadstart = function () {
                return _this7.onStartLoadingCallback();
            };
            this.mediaNode.onended = function () {
                return _this7.onEndPlayingCallback();
            };
            this.mediaNode.onplay = function () {
                return _this7._onPlayCallback();
            };
            this.mediaNode.onpause = function () {
                return _this7._onPausedCallback();
            };

            if (this._isMobileSafari() || this._isIosMlibro() || this._isIOSWebViewUsingAppleWebKit()) {
                this.mediaNode.onloadedmetadata = function () {
                    return _this7.onEndLoadingCallback();
                };
            } else {
                this.mediaNode.oncanplay = function () {
                    return _this7.onEndLoadingCallback();
                };
            }
        }
    }, {
        key: "_disableEventsHandling",
        value: function _disableEventsHandling() {
            this.mediaNode.onloadstart = null;
            this.mediaNode.oncanplay = null;
            this.mediaNode.onended = null;
            this.mediaNode.onplay = function () {
                return null;
            };
            this.mediaNode.onpause = function () {
                return null;
            };
            this.mediaNode.onloadedmetadata = function () {};
        }
    }, {
        key: "_getDuration",
        value: function _getDuration() {
            var _this8 = this;

            // faster resolution then
            // this.mediaNode.ondurationchange = () =&gt; this.onDurationChangeCallback(this.mediaNode.duration)
            return new Promise(function (resolve) {
                var playerMock = new Audio(_this8.mediaNode.src);
                playerMock.addEventListener("durationchange", function () {
                    if (this.duration != Infinity) {
                        resolve(this.duration);
                        playerMock.src = "";
                        playerMock.remove();
                    }
                }, false);
                playerMock.load();
                playerMock.currentTime = 24 * 60 * 60; // fake big time
                playerMock.volume = 0;
            });
        }
    }, {
        key: "_isIOSWebViewUsingAppleWebKit",
        value: function _isIOSWebViewUsingAppleWebKit() {
            var userAgent = window.navigator.userAgent.toLowerCase(),
                safari = /safari/.test(userAgent),
                ios = /iphone|ipod|ipad/.test(userAgent),
                appleWebKit = /applewebkit/.test(userAgent);
            var webView = ios &amp;&amp; !safari;

            return webView &amp;&amp; appleWebKit;
        }
    }, {
        key: "_isMobileSafari",
        value: function _isMobileSafari() {
            return window.DevicesUtils.getBrowserVersion().toLowerCase().indexOf("safari") &gt; -1 &amp;&amp; window.MobileUtils.isSafariMobile(navigator.userAgent);
        }
    }, {
        key: "_isIosMlibro",
        value: function _isIosMlibro() {
            return this.isMlibro &amp;&amp; window.MobileUtils.isSafariMobile(navigator.userAgent);
        }
    }, {
        key: "_isNotOnlineResources",
        value: function _isNotOnlineResources(source) {
            return !(source.startsWith("www.") || source.startsWith("http://") || source.startsWith("https://"));
        }
    }, {
        key: "_onPlayCallback",
        value: function _onPlayCallback() {
            this._sendEventCallback('playing');
        }
    }, {
        key: "_onPausedCallback",
        value: function _onPausedCallback() {
            this._sendEventCallback('stop');
        }
    }, {
        key: "_sendEventCallback",
        value: function _sendEventCallback(value) {
            if (this.eventBus) {
                var eventData = {
                    'source': this.sourceID,
                    'item': this.item,
                    'value': value,
                    'score': ''
                };
                this.eventBus.sendEvent('ValueChanged', eventData);
            }
        }
    }, {
        key: "_createMediaNode",
        value: function _createMediaNode() {
            throw new Error("GetMediaNode accessor is not implemented");
        }
    }, {
        key: "_onTimeUpdateEvent",
        value: function _onTimeUpdateEvent(event) {
            if (this.onTimeUpdateCallback) {
                this.onTimeUpdateCallback(event);
            }
        }
    }]);

    return BasePlayer;
}(_Player2.Player);

/***/ }),
/* 46 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Player = exports.Player = function () {
    function Player() {
        _classCallCheck(this, Player);

        if (this.constructor === Player) throw new Error("Cannot create an instance of Player abstract class");
    }

    _createClass(Player, [{
        key: "setRecording",
        value: function setRecording(source) {
            throw new Error("SetRecording method is not implemented");
        }
    }, {
        key: "startPlaying",
        value: function startPlaying() {
            throw new Error("StartPlaying method is not implemented");
        }
    }, {
        key: "stopPlaying",
        value: function stopPlaying() {
            throw new Error("StopPlaying method is not implemented");
        }
    }, {
        key: "pausePlaying",
        value: function pausePlaying() {
            throw new Error("pausePlaying method is not implemented");
        }
    }, {
        key: "setProgress",
        value: function setProgress(progress) {
            throw new Error("setProgress method is not implemented");
        }
    }, {
        key: "startStreaming",
        value: function startStreaming(stream) {
            throw new Error("StartStreaming method is not implemented");
        }
    }, {
        key: "stopStreaming",
        value: function stopStreaming() {
            throw new Error("StopStreaming method is not implemented");
        }
    }, {
        key: "getCurrentTime",
        value: function getCurrentTime() {
            throw new Error("getCurrentTime method is not implemented");
        }
    }, {
        key: "setEventBus",
        value: function setEventBus(eventBus, sourceID) {
            throw new Error("setEventBus method is not implemented");
        }
    }, {
        key: "setIsMlibro",
        value: function setIsMlibro(isMlibro) {
            throw new Error("setIsMlibro method is not implemented");
        }
    }, {
        key: "reset",
        value: function reset() {
            throw new Error("Reset method is not implemented");
        }
    }, {
        key: "destroy",
        value: function destroy() {
            throw new Error("Destroy method is not implemented");
        }
    }, {
        key: "onStartLoading",
        set: function set(callback) {
            this.onStartLoadingCallback = function () {
                return callback();
            };
        }
    }, {
        key: "onEndLoading",
        set: function set(callback) {
            this.onEndLoadingCallback = function () {
                return callback();
            };
        }
    }, {
        key: "onEndPlaying",
        set: function set(callback) {
            this.onEndPlayingCallback = function () {
                return callback();
            };
        }
    }, {
        key: "onDurationChange",
        set: function set(callback) {
            this.onDurationChangeCallback = function (duration) {
                return callback(duration);
            };
        }
    }, {
        key: "onTimeUpdate",
        set: function set(callback) {
            this.onTimeUpdateCallback = function (event) {
                return callback(event);
            };
        }
    }]);

    return Player;
}();

/***/ }),
/* 47 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.DefaultRecordingPlayButton = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _Button2 = __webpack_require__(1);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var DefaultRecordingPlayButton = exports.DefaultRecordingPlayButton = function (_Button) {
    _inherits(DefaultRecordingPlayButton, _Button);

    function DefaultRecordingPlayButton(_ref) {
        var $view = _ref.$view,
            state = _ref.state,
            defaultRecording = _ref.defaultRecording;

        _classCallCheck(this, DefaultRecordingPlayButton);

        var _this = _possibleConstructorReturn(this, (DefaultRecordingPlayButton.__proto__ || Object.getPrototypeOf(DefaultRecordingPlayButton)).call(this, $view));

        _this.state = state;
        _this.defaultRecording = defaultRecording;
        return _this;
    }

    _createClass(DefaultRecordingPlayButton, [{
        key: "destroy",
        value: function destroy() {
            _get(DefaultRecordingPlayButton.prototype.__proto__ || Object.getPrototypeOf(DefaultRecordingPlayButton.prototype), "destroy", this).call(this);
            this.state = null;
        }
    }, {
        key: "_eventHandler",
        value: function _eventHandler() {
            if ((this.state.isLoaded() || this.state.isLoadedDefaultRecording()) &amp;&amp; this.defaultRecording != "") this._startPlaying();else if (this.state.isPlayingDefaultRecording()) this._stopPlaying();
        }
    }, {
        key: "_startPlaying",
        value: function _startPlaying() {
            this.$view.addClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onStartPlayingCallback();
        }
    }, {
        key: "_stopPlaying",
        value: function _stopPlaying() {
            this.$view.removeClass(_CssClasses.CSS_CLASSES.SELECTED);
            this.onStopPlayingCallback();
        }
    }, {
        key: "onStartPlaying",
        set: function set(callback) {
            this.onStartPlayingCallback = callback;
        }
    }, {
        key: "onStopPlaying",
        set: function set(callback) {
            this.onStopPlayingCallback = callback;
        }
    }]);

    return DefaultRecordingPlayButton;
}(_Button2.Button);

/***/ }),
/* 48 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.DefaultKeyboardController = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _BaseKeyboardController = __webpack_require__(6);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var DefaultKeyboardController = exports.DefaultKeyboardController = function (_BaseKeyboardControll) {
    _inherits(DefaultKeyboardController, _BaseKeyboardControll);

    function DefaultKeyboardController() {
        _classCallCheck(this, DefaultKeyboardController);

        return _possibleConstructorReturn(this, (DefaultKeyboardController.__proto__ || Object.getPrototypeOf(DefaultKeyboardController)).apply(this, arguments));
    }

    _createClass(DefaultKeyboardController, [{
        key: "markDefaultRecordingPlayButton",
        value: function markDefaultRecordingPlayButton() {
            this.markCurrentElement(0);
        }
    }, {
        key: "markRecordingButton",
        value: function markRecordingButton() {
            this.markCurrentElement(1);
        }
    }, {
        key: "markPlayButton",
        value: function markPlayButton() {
            this.markCurrentElement(2);
        }
    }, {
        key: "readElement",
        value: function readElement(element) {
            var $element = this.getTarget(element, false);

            if ($element.hasClass(_CssClasses.CSS_CLASSES.DEFAULT_RECORDING_PLAY_BUTTON)) {
                this._speakDefaultRecordingPlayButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.RECORDING_BUTTON)) {
                this._speakRecordingButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.PLAY_BUTTON)) {
                this._speakPlayButtonTTS($element);
            }
        }
    }, {
        key: "_speakDefaultRecordingPlayButtonTTS",
        value: function _speakDefaultRecordingPlayButtonTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.DefaultRecordingPlayButton);

            if (this._activationState.isInactive()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }]);

    return DefaultKeyboardController;
}(_BaseKeyboardController.BaseKeyboardController);

/***/ }),
/* 49 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.ExtendedKeyboardController = undefined;

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

var _BaseKeyboardController = __webpack_require__(6);

var _CssClasses = __webpack_require__(0);

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call &amp;&amp; (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" &amp;&amp; superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass &amp;&amp; superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var ExtendedKeyboardController = exports.ExtendedKeyboardController = function (_BaseKeyboardControll) {
    _inherits(ExtendedKeyboardController, _BaseKeyboardControll);

    function ExtendedKeyboardController(elements, columnsCount, model, mediaState, activationState, speak, speakAndExecuteCallback) {
        _classCallCheck(this, ExtendedKeyboardController);

        var _this = _possibleConstructorReturn(this, (ExtendedKeyboardController.__proto__ || Object.getPrototypeOf(ExtendedKeyboardController)).call(this, elements, columnsCount, model, mediaState, activationState, speak, speakAndExecuteCallback));

        _this._resetDialogLabels = model.resetDialogLabels;
        _this._disableRecording = model.disableRecording;
        return _this;
    }

    _createClass(ExtendedKeyboardController, [{
        key: "markRecordingButton",
        value: function markRecordingButton() {
            this.markCurrentElement(0);
        }
    }, {
        key: "markPlayButton",
        value: function markPlayButton() {
            this.markCurrentElement(1);
        }
    }, {
        key: "markResetButton",
        value: function markResetButton() {
            this.markCurrentElement(2);
        }
    }, {
        key: "markDownloadButton",
        value: function markDownloadButton() {
            this.markCurrentElement(3);
        }
    }, {
        key: "_performFirstEnterEvent",
        value: function _performFirstEnterEvent() {
            this.keyboardNavigationActive = true;
            var $element = this.getTarget(this.keyboardNavigationElements[0], false);

            if (this._isKeyboardNavigationBlocked()) {
                this._markActiveElement();
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.DIALOG_TEXT)) {
                this.markDialogTextAndReadResetDialogTTS();
            } else {
                this._markAndReadFirstDisplayedElement();
            }
        }
    }, {
        key: "markDialogTextAndReadResetDialogTTS",
        value: function markDialogTextAndReadResetDialogTTS() {
            this.markCurrentElement(0);
            this._speakResetDialogTTS();
        }
    }, {
        key: "readElement",
        value: function readElement(element) {
            var $element = this.getTarget(element, false);

            if ($element.hasClass(_CssClasses.CSS_CLASSES.RECORDING_BUTTON)) {
                this._speakRecordingButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.PLAY_BUTTON)) {
                this._speakPlayButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.RESET_BUTTON)) {
                this._speakResetButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.DOWNLOAD_BUTTON)) {
                this._speakDownloadButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.DIALOG_TEXT)) {
                this._speakDialogTextTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.CONFIRM_BUTTON)) {
                this._speakConfirmButtonTTS($element);
            } else if ($element.hasClass(_CssClasses.CSS_CLASSES.DENY_BUTTON)) {
                this._speakDenyButtonTTS($element);
            }
        }
    }, {
        key: "_speakResetButtonTTS",
        value: function _speakResetButtonTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.ResetButton);

            if (this._activationState.isInactive()) {
                this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.Disabled);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_speakDownloadButtonTTS",
        value: function _speakDownloadButtonTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.DownloadButton);

            if (this._activationState.isInactive()) {
                this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.Disabled);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_speakResetDialogTTS",
        value: function _speakResetDialogTTS() {
            var textVoiceObject = [];

            this._pushResetDialogTextMessageToTextVoiceObject(textVoiceObject);
            this._pushResetDialogConfirmMessageToTextVoiceObject(textVoiceObject);
            this._pushResetDialogDenyMessageToTextVoiceObject(textVoiceObject);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_speakDialogTextTTS",
        value: function _speakDialogTextTTS($element) {
            var textVoiceObject = [];

            this._pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, this.speechTexts.ResetDialog);

            this._pushResetDialogTextMessageToTextVoiceObject(textVoiceObject);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_pushResetDialogTextMessageToTextVoiceObject",
        value: function _pushResetDialogTextMessageToTextVoiceObject(textVoiceObject) {
            this._pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, this._resetDialogLabels.resetDialogText.resetDialogLabel);
        }
    }, {
        key: "_speakConfirmButtonTTS",
        value: function _speakConfirmButtonTTS($element) {
            var textVoiceObject = [];

            this._pushResetDialogConfirmMessageToTextVoiceObject(textVoiceObject);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_pushResetDialogConfirmMessageToTextVoiceObject",
        value: function _pushResetDialogConfirmMessageToTextVoiceObject(textVoiceObject) {
            this._pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, this._resetDialogLabels.resetDialogConfirm.resetDialogLabel);
        }
    }, {
        key: "_speakDenyButtonTTS",
        value: function _speakDenyButtonTTS($element) {
            var textVoiceObject = [];

            this._pushResetDialogDenyMessageToTextVoiceObject(textVoiceObject);

            if (this._isAddonDisabled()) {
                this._pushDisabledMessageToTextVoiceObject(textVoiceObject);
            }

            this._speak(textVoiceObject);
        }
    }, {
        key: "_pushResetDialogDenyMessageToTextVoiceObject",
        value: function _pushResetDialogDenyMessageToTextVoiceObject(textVoiceObject) {
            this._pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, this._resetDialogLabels.resetDialogDeny.resetDialogLabel);
        }
    }, {
        key: "onStopRecording",
        value: function onStopRecording() {
            _get(ExtendedKeyboardController.prototype.__proto__ || Object.getPrototypeOf(ExtendedKeyboardController.prototype), "onStopRecording", this).call(this);

            if (!this._disableRecording) {
                this.nextElement();
            }
        }
    }, {
        key: "onStopRecordingWhenSoundEffect",
        value: function onStopRecordingWhenSoundEffect() {
            _get(ExtendedKeyboardController.prototype.__proto__ || Object.getPrototypeOf(ExtendedKeyboardController.prototype), "onStopRecordingWhenSoundEffect", this).call(this);

            if (!this._disableRecording) {
                this.nextElement();
            }
        }
    }]);

    return ExtendedKeyboardController;
}(_BaseKeyboardController.BaseKeyboardController);

/***/ }),
/* 50 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var MP3ConvertHandler = exports.MP3ConvertHandler = function () {
    function MP3ConvertHandler(playerController) {
        _classCallCheck(this, MP3ConvertHandler);

        this.worker = null;
        this.scriptURL = null;
        this.isValid = false;
        this.validationTimoutID = null;
        this.origin = document.location.origin;
        this.lameScriptURL = "";

        if (!this.isSupported()) {
            console.log('Your browser doesn\'t support web workers.');
            return;
        }
        var context = playerController.getContextMetadata();
        if (context != null) {
            if ("rootDirectory" in context) {
                this.origin = context["rootDirectory"];
            }
            if ("lameScriptURL" in context) {
                this.lameScriptURL = context["lameScriptURL"];
            }
        }

        var scriptBlob = this.createBlobWithScript();
        this.scriptURL = URL.createObjectURL(scriptBlob);
        this._validateScript();
    }

    _createClass(MP3ConvertHandler, [{
        key: "isSupported",
        value: function isSupported() {
            return !!window.Worker;
        }
    }, {
        key: "isWorkerExist",
        value: function isWorkerExist() {
            return !!this.worker;
        }
    }, {
        key: "_validateScript",
        value: function _validateScript() {
            var _this = this;

            var self = this;
            return new Promise(function (resolve) {
                _this.worker = new Worker(_this.scriptURL);
                _this.worker.onmessage = function (e) {
                    if (e.data === "WORKER STARTED") {
                        self.isValid = true;
                        resolve(true);
                    } else {
                        console.log('Error occurred for Web worker in Media Recorder: ' + e.data);
                        self.isValid = false;
                        resolve(false);
                    }
                };
                _this.worker.postMessage({
                    cmd: "validate",
                    origin: self.origin,
                    lameScriptURL: self.lameScriptURL
                });
                _this.validationTimoutID = setTimeout(function () {
                    if (!self.isValid) {
                        console.log('Lib for web worker in Media Recorder is unreachable.');
                        self.isValid = false;
                        _this.terminateWorkerProcess();
                        resolve(false);
                    }
                }, 5000);
            });
        }
    }, {
        key: "execute",
        value: function execute(numberOfChannels, sampleRate, sampleLength, leftChannelData, rightChannelData) {
            var _this2 = this;

            return new Promise(function (resolve, reject) {
                if (!_this2.isValid) {
                    reject("Not valid worker");
                }
                if (_this2.isWorkerExist()) {
                    _this2.terminateWorkerProcess();
                }
                _this2.worker = new Worker(_this2.scriptURL);
                _this2.worker.onmessage = function (e) {
                    resolve(e.data);
                };
                _this2.worker.postMessage({
                    cmd: "start",
                    data: {
                        numberOfChannels: numberOfChannels,
                        sampleRate: sampleRate,
                        sampleLength: sampleLength,
                        leftChannelData: leftChannelData,
                        rightChannelData: rightChannelData
                    },
                    origin: _this2.origin,
                    lameScriptURL: _this2.lameScriptURL
                });
            });
        }
    }, {
        key: "createBlobWithScript",
        value: function createBlobWithScript() {
            var script = "\n            addEventListener(\"message\", function(e) {\n                let data = e.data;\n                if (!data.origin) {\n                    postMessage(\"Unknown origin\");\n                    return;\n                }\n                const lameScriptURL = data.lameScriptURL.length &gt; 0 ? data.lameScriptURL : data.origin + \"/media/icplayer/libs/lame.min.js\";\n                try {\n                    importScripts(lameScriptURL);\n                } catch (e) {\n                    postMessage(\"Library lame.min.js is unreachable\");\n                    return;\n                }\n\n                switch (data.cmd) {\n                    case \"validate\":\n                        postMessage(\"WORKER STARTED\");\n                        break;\n                    case \"start\":\n                        postMessage(_encode(\n                            data.data.numberOfChannels,\n                            data.data.sampleRate,\n                            data.data.sampleLength,\n                            data.data.leftChannelData,\n                            data.data.rightChannelData\n                        ));\n                        break;\n                    default:\n                        postMessage(\"Unknown command: \" + data.cmd);\n                };\n            }, false);\n\n            function _encode(channels, sampleRate, sampleLength, leftChannelData, rightChannelData) {\n                let buffer = [];\n                let mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 96); //third value determinate bitrate\n\n                const maxSamples = 1152;\n                for (let i = 0; i &lt; sampleLength; i += maxSamples) {\n                    let leftChunk = leftChannelData.subarray(i, i + maxSamples);\n                    let rightChunk = rightChannelData.subarray(i, i + maxSamples);\n\n                    let mp3buf = mp3enc.encodeBuffer(leftChunk, rightChunk);\n                    if (mp3buf.length &gt; 0) {\n                        buffer.push(new Int8Array(mp3buf));\n                    }\n                }\n                let d = mp3enc.flush();\n                if (d.length &gt; 0){\n                    buffer.push(new Int8Array(d));\n                }\n\n                return new Blob(buffer, {type: \"audio/mpeg-3\"});\n            }\n        ";
            return new Blob([script], { type: 'application/javascript' });
        }
    }, {
        key: "destroy",
        value: function destroy() {
            if (this.isWorkerExist()) {
                this.terminateWorkerProcess();
            }
            if (this.scriptURL) {
                URL.revokeObjectURL(this.scriptURL);
                this.scriptURL = null;
            }
            if (this.validationTimoutID) {
                clearTimeout(this.validationTimoutID);
                this.validationTimoutID = null;
            }
            this.isValid = false;
        }
    }, {
        key: "terminateWorkerProcess",
        value: function terminateWorkerProcess() {
            this.worker.terminate();
            this.worker = null;
        }
    }]);

    return MP3ConvertHandler;
}();

/***/ })
/******/ ]);
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="MenuPanel" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Menu Items" isDefault="true" name="MenuItems" nameLabel="MenuPanel_property_menu_items" type="list">
            <property displayName="Id" name="id" nameLabel="MenuPanel_property_id" type="string"/>
            <property displayName="Title" name="title" nameLabel="MenuPanel_property_title" type="string"/>
            <property displayName="Parent Id" name="parentId" nameLabel="MenuPanel_property_parent_id" type="string"/>

            <property displayName="Image" name="image" nameLabel="MenuPanel_property_image" type="image"/>

            <property displayName="Is Expanded" name="isExpanded" nameLabel="MenuPanel_property_is_expanded" type="boolean"/>
            <property displayName="Is Selected" name="isSelected" nameLabel="MenuPanel_property_is_selected" type="boolean"/>
            <property displayName="Is Disabled" name="isDisabled" nameLabel="MenuPanel_property_is_disabled" type="boolean"/>
        </property>

        <property name="HorizontalAlignment" nameLabel="MenuPanel_property_horizontal_alignment" type="{default,left,center,right}"/>
        <property name="VerticalAlignment" nameLabel="MenuPanel_property_vertical_alignment" type="{default,top,center,bottom}"/>
        <property displayName="Auto Expand" name="autoExpand" nameLabel="MenuPanel_property_auto_expand" type="boolean"/>
        <property name="Disable" nameLabel="MenuPanel_property_disable" type="boolean"/>
    </model>
<css>.menu-panel-wrapper{
    position: absolute;
    width: 100%;
    height: 100%;
}
.menu-panel-item{
    display: inline-block;
    width: 100%;
    cursor: pointer;
    border: 1px solid #aaaaaa;
}
.menu-panel-wrapper .children{
    position: absolute;
    left: 100%;
}
.menu-panel-wrapper .horizontal-right .children{
    position: absolute;
    right: 100%;
}
.menu-panel-wrapper .disabled{
    cursor: default;
    border: 1px solid #dddddd;
}
.menu-panel-errors{
    position: absolute;
}
.menu-panel-errors p.mpError{
    color: red;
    font-size: 12px;
}
</css><view>&lt;div class="menu-panel-wrapper"&gt;&lt;/div&gt;
&lt;div class="menu-panel-errors"&gt;&lt;/div&gt;
</view><preview>&lt;div class="menu-panel-wrapper"&gt;&lt;/div&gt;
&lt;div class="menu-panel-errors"&gt;&lt;/div&gt;
</preview><presenter>function AddonMenuPanel_create(){
    /* ChangeLog
     - added &lt;img&gt; to appended div in drawElements
     - changed select to selectCommand in presenter.executeCommand, same for deselect
     */

    var presenter = function(){};

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.presentation = controller.getPresentation();
        presenter.currentIndex = controller.getCurrentPageIndex();
    };

    presenter.run = function(view, model){
        presenterLogic(view, model);
        presenter.setVisibility(presenter.isVisibleByDefault);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    function presenterLogic(view, model) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        presenter.isError = false;

        presenter.HorizontalAlignment = presenter.HorizontalAlignmentByDefault = model.HorizontalAlignment;
        presenter.VerticalAlignment = presenter.VerticalAlignmentByDefault = model.VerticalAlignment;
        presenter.setAlignment(presenter.HorizontalAlignment, presenter.VerticalAlignment);

        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.isVisibleByDefault = presenter.isVisible;
        presenter.originalDisplay = presenter.$view.css('display') ? presenter.$view.css('display') : 'block';

        presenter.isDisabled = ModelValidationUtils.validateBoolean(model["Disable"]);
        presenter.isDisabledByDefault = presenter.isDisabled;
        presenter.setAbilityAll(!presenter.isDisabledByDefault);

        presenter.menuItemsByDefault = presenter.model.MenuItems;
        presenter.menuItems = presenter.buildElementsArray(presenter.model.MenuItems);
        presenter.drawElements();

        presenter.autoExpand = ModelValidationUtils.validateBoolean(model["autoExpand"]);

        presenter.$view.on('click', function(e){e.stopPropagation();});
    }

    presenter.buildElementsArray = function(itemsArray){
        if(!presenter.isError){
            var nestedItems = new Array;

            $(itemsArray).each(function(i, menuItem){
                if(menuItem.id == ''){
                    presenter.errorHandler('Id in item '+(i+1)+' is empty!');
                    return false;
                }

                var isSelected = ModelValidationUtils.validateBoolean(menuItem.isSelected);
                var isExpanded = ModelValidationUtils.validateBoolean(menuItem.isExpanded);
                var isDisabled = ModelValidationUtils.validateBoolean(menuItem.isDisabled);

                nestedItems[i] = {};
                nestedItems[i].id = menuItem.id;
                nestedItems[i].parentId = menuItem.parentId;
                nestedItems[i].title = menuItem.title;
                nestedItems[i].image = menuItem.image;
                nestedItems[i].isSelected = isSelected;
                nestedItems[i].isExpanded = isExpanded;
                nestedItems[i].isDisabled = isDisabled;
                nestedItems[i].children = [];

                if(menuItem.parentId != ''){
                    $(nestedItems).each(function(i, menuItem2){
                        if(menuItem2['id'] == menuItem['parentId']){
                            nestedItems[i].children.push(menuItem.id);
                        }
                    });
                }
            });

            return nestedItems;
        }
    }

    presenter.drawElements = function(){
        if(!presenter.isError){
            var wrapper = presenter.$view.find('.menu-panel-wrapper')[0];
            $(wrapper).html('')

            $(presenter.menuItems).each(function(i, menuItem){
                var elementClassPrefix = 'item';

                var parent = presenter.menuItems[i].parentId == '' ? wrapper : presenter.$view.find('.'+elementClassPrefix+presenter.menuItems[i].parentId+'.children')[0];
                var content = presenter.menuItems[i].title;

                var selected = presenter.menuItems[i].isSelected ? ' selected' : '';
                var disabled = presenter.menuItems[i].isDisabled ? ' disabled' : '';

                var div = '&lt;div class="menu-panel-item '+elementClassPrefix+menuItem.id+selected+disabled+'"&gt;';
                if(presenter.menuItems[i].image != ''){
                    div += '&lt;img src="'+presenter.menuItems[i].image+'" alt="" /&gt;';
                }
                div += content+'&lt;/div&gt;';

                $(parent).append(div);

                if(presenter.menuItems[i].children != ''){
                    var visibility = presenter.menuItems[i].isExpanded ? 'visible' : 'hidden';
                    var display = presenter.menuItems[i].isExpanded ? 'block' : 'none';
                    var expanded = presenter.menuItems[i].isExpanded ? 'expanded' : 'contracted';

                    $(parent).append('&lt;div class="'+elementClassPrefix+menuItem.id+' children '+expanded+'" style="visibility: '+visibility+'; display:'+display+';"&gt;&lt;/div&gt;');
                }

                var clickableElement = presenter.$view.find('.'+elementClassPrefix+menuItem.id)[0];
                var clickData = {item: i};
                $(clickableElement).on('click', clickData, presenter.clickHandler);

            });
        }
    }

    presenter.clickHandler = function(e){
        if(!presenter.isDisabled &amp;&amp; !presenter.menuItems[e.data.item].isDisabled){
            var value;

            if(presenter.menuItems[e.data.item].isSelected){
                presenter.menuItems[e.data.item].isSelected = false;
                $(presenter.$view.find('.item'+presenter.menuItems[e.data.item].id)[0]).removeClass("selected");
                value = 0;
            }else{
                presenter.menuItems[e.data.item].isSelected = true;
                $(presenter.$view.find('.item'+presenter.menuItems[e.data.item].id)[0]).addClass("selected");
                value = 1;
            }

            presenter.sendEventData(presenter.menuItems[e.data.item].id,value);

            //Auto Expand control
            if(presenter.autoExpand){
                var parentId = presenter.menuItems[e.data.item].parentId;
                var parent = getItemById(parentId);
                var siblings = [];

                if(parentId != ''){
                    $(presenter.menuItems[parent].children).each(function(i,child){
                        if(child != presenter.menuItems[e.data.item].id){
                            siblings.push(child);
                        }
                    });
                }else{
                    $(presenter.menuItems).each(function(i,menuItem){
                        if(menuItem.parentId == '' &amp;&amp; menuItem.id != presenter.menuItems[e.data.item].id){
                            siblings.push(menuItem.id);
                        }
                    });
                }

                if(presenter.menuItems[e.data.item].children.length != 0){
                    $(siblings).each(function(i,sibling){
                        //sprawdzić, czy sibling ma dzieci!
                        var item = getItemById(sibling);
                        if(presenter.menuItems[item].children.length != 0){
                            presenter.contract(sibling);
                            presenter.deselect(sibling);
                        }
                    });
                }

                presenter.toggleExpand(presenter.menuItems[e.data.item].id);
            }

        }
    }

    presenter.errorHandler = function(msg){
        presenter.isError = true;
        presenter.$view.find('.menu-panel-errors').append('&lt;p class="mpError"&gt;'+msg+'&lt;/p&gt;');
    };

    presenter.executeCommand = function(name, params){
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enableCommand,
            'disable': presenter.disableCommand,
            'select': presenter.selectCommand,
            'deselect': presenter.deselectCommand,
            'setAlignment': presenter.setAlignmentCommand,
            'expand': presenter.expandCommand,
            'contract': presenter.contractCommand,
            'toggleExpand': presenter.toggleExpandCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.createEventData = function(item,value) {
        return {
            source : presenter.addonID,
            item : item,
            value : value,
            score : ''
        };
    };
    presenter.sendEventData = function (item,value) {
        var eventData = presenter.createEventData(item,value);
        if (presenter.playerController !== null) {
            presenter.playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.select = function(item){
        var i = getItemById(item);
        presenter.menuItems[i].isSelected = true;
        $(presenter.$view.find('.item'+presenter.menuItems[i].id)[0]).addClass("selected");
    }
    presenter.selectCommand = function(params){
        presenter.select(params[0]);
    }
    presenter.deselect = function(item){
        var i = getItemById(item);
        presenter.menuItems[i].isSelected = false;
        $(presenter.$view.find('.item'+presenter.menuItems[i].id)[0]).removeClass("selected");
    }
    presenter.deselectCommand = function(params){
        presenter.deselect(params[0]);
    }

    presenter.toggleExpand = function(item){
        var i = getItemById(item);
        presenter.menuItems[i].isExpanded ? presenter.contract(item) : presenter.expand(item);
    }
    presenter.toggleExpandCommand = function(params){
        presenter.toggleExpand(params[0]);
    }

    presenter.expand = function(item){
        var i = getItemById(item);
        var itemClasss = '.item'+presenter.menuItems[i].id+'.children';
        presenter.menuItems[i].isExpanded = true;
        $(presenter.$view.find(itemClasss)[0]).addClass('expanded');
        $(presenter.$view.find(itemClasss)[0]).removeClass('contracted');
        $(presenter.$view.find(itemClasss)[0]).css('visibility','visible');
        $(presenter.$view.find(itemClasss)[0]).css('display','block');
        //show this item's parents if exists
        if(presenter.menuItems[i].parentId != ''){
            presenter.expand(presenter.menuItems[i].parentId);
        }

    }
    presenter.expandCommand = function(params){
        presenter.expand(params[0]);
    }
    presenter.contract = function(item){
        var i = getItemById(item);
        var itemClasss = '.item'+presenter.menuItems[i].id+'.children';
        presenter.menuItems[i].isExpanded = false;
        $(presenter.$view.find(itemClasss)[0]).removeClass('expanded');
        $(presenter.$view.find(itemClasss)[0]).addClass('contracted');
        $(presenter.$view.find(itemClasss)[0]).css('visibility','hidden');
        $(presenter.$view.find(itemClasss)[0]).css('display','none');
        //hide all children
        $(presenter.menuItems[i].children).each(function(i,child){
            presenter.contract(child);

            if(presenter.autoExpand){
                var childItem = getItemById(child);
                if(presenter.menuItems[childItem].children.length != 0){
                    presenter.deselect(child);
                }
            }
        });
    }
    presenter.contractCommand = function(params){
        presenter.contract(params[0]);
    }

    presenter.setAlignment = function(horizontal, vertical){
        presenter.HorizontalAlignment = horizontal;
        presenter.VerticalAlignment = vertical;

        switch(horizontal){
            case "left": {
                presenter.$view.css({"left":"0","right":"auto"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('horizontal-right horizontal-center');
                presenter.$view.find('.menu-panel-wrapper').addClass('horizontal-left');
                break;
            }
            case "right": {
                presenter.$view.css({"left":"auto","right":"0"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('horizontal-left horizontal-center');
                presenter.$view.find('.menu-panel-wrapper').addClass('horizontal-right');
                break;
            }
            case "center": {
                var pageWidth = parseInt(presenter.$view.parent().css('width'),10);
                var leftPos = (pageWidth / 2) - (presenter.model.Width / 2);
                presenter.$view.css({"left":leftPos,"right":"auto"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('horizontal-right horizontal-left');
                presenter.$view.find('.menu-panel-wrapper').addClass('horizontal-center');
                break;
            }
            //Na przyszłość
            //DODAĆ obsługę defaulta, żeby można było z komendy przywrócić
            //do tego trzeba będzie pewnie zapamiętać stan pozycji z edytora
        }

        switch(vertical){
            case "top": {
                presenter.$view.css({"top":"0","bottom":"auto"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('vertical-bottom vertical-center');
                presenter.$view.find('.menu-panel-wrapper').addClass('vertical-top');
                break;
            }
            case "bottom": {
                presenter.$view.css({"top":"auto","bottom":"0"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('vertical-top vertical-center');
                presenter.$view.find('.menu-panel-wrapper').addClass('vertical-bottom');
                break;
            }
            case "center": {
                var pageHeight = parseInt(presenter.$view.parent().css('height'),10);
                var topPos = (pageHeight / 2) - (presenter.model.Height / 2);
                presenter.$view.css({"top":topPos,"bottom":"auto"});
                presenter.$view.find('.menu-panel-wrapper').removeClass('vertical-bottom vertical-top');
                presenter.$view.find('.menu-panel-wrapper').addClass('vertical-center');
                break;
            }
            //Na przyszłość
            //DODAĆ obsługę defaulta, żeby można było z komendy przywrócić
            //do tego trzeba będzie pewnie zapamiętać stan pozycji z edytora
        }
    }
    presenter.setAlignmentCommand = function (params) {
        presenter.setAlignment(params[0], params[1]);
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };
    presenter.hide = function(){
        presenter.setVisibility(false);
    };
    presenter.setVisibility = function(isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.$view.css("display", isVisible ? presenter.originalDisplay : "none");
    };

    presenter.enable = function(item) {
        if(typeof item === 'undefined'){
            presenter.setAbilityAll(true);
        }else{
            var i = getItemById(item);
            presenter.menuItems[i].isDisabled = false;
            $(presenter.$view.find('.item'+presenter.menuItems[i].id)[0]).removeClass("disabled");
        }
    };
    presenter.enableCommand = function(params) {
        presenter.enable(params[0]);
    };
    presenter.disable = function(item){
        if(typeof item === 'undefined'){
            presenter.setAbilityAll(false);
        }else{
            var i = getItemById(item);
            presenter.menuItems[i].isDisabled = true;
            $(presenter.$view.find('.item'+presenter.menuItems[i].id)[0]).addClass("disabled");
        }
    };
    presenter.disableCommand = function(params) {
        presenter.disable(params[0]);
    };
    presenter.setAbilityAll = function(ability) {
        presenter.isDisabled = !ability;
        var wrapper = presenter.$view.find('.menu-panel-wrapper')[0];
        if(ability){
            $(wrapper).removeClass("disabled");
        }else{
            $(wrapper).addClass("disabled");
        }
    };


    presenter.reset = function(){
        presenter.setVisibility(presenter.isVisibleByDefault);
        presenter.setAbilityAll(!presenter.isDisabledByDefault);
        presenter.setAlignment(presenter.HorizontalAlignmentByDefault, presenter.VerticalAlignmentByDefault);
        presenter.menuItems = presenter.buildElementsArray(presenter.menuItemsByDefault);
        presenter.drawElements();
    };

    presenter.getState = function(){
        return JSON.stringify({
            isVisible: presenter.isVisible,
            isDisabled: presenter.isDisabled,
            HorizontalAlignment: presenter.HorizontalAlignment,
            VerticalAlignment: presenter.VerticalAlignment,
            menuItems: presenter.menuItems
        });
    };
    presenter.setState = function(stateString){
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        var state = JSON.parse(stateString);
        presenter.isVisible = state.isVisible;
        presenter.isDisabled = state.isDisabled;
        presenter.HorizontalAlignment = state.HorizontalAlignment;
        presenter.VerticalAlignment = state.VerticalAlignment;
        presenter.menuItems = state.menuItems;

        presenter.setVisibility(presenter.isVisible);
        presenter.setAbilityAll(!presenter.isDisabled);
        presenter.setAlignment(presenter.HorizontalAlignment, presenter.VerticalAlignment);
        presenter.drawElements();
    };

    function getItemById(id){
        var itemToReturn = false;
        $(presenter.menuItems).each(function(i, menuItem){
            if(menuItem.id === id){
                itemToReturn = i;
                return false;
            }
        });
        return itemToReturn;
    }

    presenter.setShowErrorsMode = function(){ };
    presenter.setWorkMode = function(){ };
    presenter.showAnswers = function(){ };
    presenter.hideAnswers = function(){ };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="ModelViewer3D" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="model" nameLabel="Model_Viewer_model" type="file"/>
        <property name="modelIOS" nameLabel="Model_Viewer_model_iOS" type="file"/>
        <property name="poster" nameLabel="Model_Viewer_poster" type="file"/>
        <property name="annotations" nameLabel="Model_Viewer_annotations" type="text"/>
        <property name="environmentImage" nameLabel="Model_Viewer_environmentImage" type="image"/>
        <property name="skyboxImage" nameLabel="Model_Viewer_skyboxImage" type="image"/>
        <property name="shadowIntensity" nameLabel="Model_Viewer_shadowIntensity" type="string"/>
        <property name="shadowSoftness" nameLabel="Model_Viewer_shadowSoftness" type="string"/>
        <property name="autoRotate" nameLabel="Model_Viewer_autoRotate" type="boolean"/>
        <property name="scale" nameLabel="Model_Viewer_scale" type="string"/>
        <property name="labelsEnabled" nameLabel="Model_Viewer_labelsEnabled" type="boolean"/>
        <property name="altText" nameLabel="Model_Viewer_altText" type="string"/>
        <property name="attributes" nameLabel="Model_Viewer_attributes" type="text"/>
        <property name="copyInfo" nameLabel="Model_Viewer_copyInfo" type="html"/>
        <property name="interactionPrompt" nameLabel="Model_Viewer_interactionPrompt" type="boolean"/>
        <property name="enableFullscreen" nameLabel="Model_Viewer_enableFullscreen" type="boolean"/>
        <property name="{library}" nameLabel="Model_Viewer_library" type="module-script" value="/file/serve/5059308589809664"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Model_Viewer_lang_attribute" type="string"/>
    </model>
<css>.md-webcam {
    display: none;
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.modelViewerWrapper {
    width: 100%;
    height: 100%;
}

model-viewer {
    user-select: none;
    width: 100%;
    height: 100%;
    background-color: transparent !important;
}

model-viewer::part(default-progress-bar) {
    background-color: #2196F3;
}

.buttonsContainer {
    position: absolute;
    top: 25px;
    left: auto;
    right: 25px;
}

.copyButton {
    position: absolute;
}

.copyMessage {
    display: none;
    position: absolute;
    top: 0;
    bottom: 75px;
    left: 0;
    right: 0;
    background-color: rgba(0, 0, 0, 0.2);
}

.copyMessage-visible {
    display: block;
}

.copyContainer {
    position: absolute;
    top: 100%;
    left: 50%;
    margin: -170px 0 0 -300px;
    padding: 40px;
    text-align: center;
    width: 520px;
    height: auto;
    max-height: 220px;
    overflow-y: auto;
    background: #fff;
    border-radius: 5px;
    box-shadow: 0px 3px 5px 1px rgba(0, 0, 0, 0.5);
    z-index: 1;
}

.copyButton,
.labelsButton,
.fullscreenButton {
    width: 75px;
    height: 50px;
    background-color: #78909C;
    margin-bottom: 10px;
}

.copyButton-selected,
.labelsButton-selected {
    background-color: #0A8DBD;
}

.Hotspot {
    background: #fff;
    border-radius: 32px;
    border: 0;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
    box-sizing: border-box;
    cursor: pointer;
    height: 24px;
    padding: 8px;
    position: relative;
    transition: opacity 0.3s;
    width: 24px;
}

.Hotspot:not([data-visible]) {
    background: transparent;
    border: 4px solid #fff;
    box-shadow: none;
    height: 32px;
    pointer-events: none;
    width: 32px;
}

.Hotspot:focus {
    border: 4px solid rgb(0, 128, 200);
    height: 32px;
    outline: none;
    width: 32px;
}

.Hotspot &gt; * {
    opacity: 1;
    transform: translateY(-50%);
}

.HotspotAnnotation {
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
    color: rgba(0, 0, 0, 0.8);
    display: block;
    font-family: Futura, Helvetica Neue, sans-serif;
    font-size: 18px;
    font-weight: 700;
    left: calc(100% + 1em);
    max-width: 200px;
    overflow-wrap: break-word;
    padding: 0.5em 1em;
    position: absolute;
    top: 50%;
    width: max-content;
}

.Hotspot:not([data-visible]) &gt; * {
    opacity: 0.5;
    pointer-events: none;
    transform: translateY(calc(-50% + 4px));
    transition: transform 0.3s, opacity 0.3s;
}

.hidden {
    visibility: hidden
}

.error {
    text-align: center;
    color: red;
}
</css><view/><preview/><presenter>function AddonModelViewer3D_create() {
    // to create version on mauthor-dev use storage.googleapis URL
    var presenter = function () { };

    presenter.wasInitiated = false;
    presenter.annotationsVisibility = "visible";
    presenter.isVisibleOnStart = true;
    presenter.areAnnotationsVisibleOnStart = true;
    presenter.fullscreen = {
        isOn: false,
        hasSupport: false,
        requestFullscreenMethod: null,
        exitFullscreenMethod: null,
    };
    presenter.isWCAGOn = false;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.run = function (view, model) {
        if (!presenter.wasInitiated) {
            presenter.init(view, model, false);
        }
    };

    presenter.createPreview = function (view, model) {
        if (!presenter.wasInitiated) {
            presenter.init(view, model, true);
            presenter.disableEvents();
        }
    };

    presenter.init = function (view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);

        const upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        presenter.isVisibleOnStart = presenter.configuration.isVisible;

        if (!presenter.configuration.isValid) {
            showErrorMessage();
            return;
        }

        presenter.extendView(isPreview);
        checkFullscreenSupport();
        !isPreview &amp;&amp; presenter.addHandlers();
        presenter.setAnnotations();
        presenter.setAutoRotation();
        presenter.setInteractionPrompt();
        presenter.handleButtons();
        presenter.handleCopyright();
        presenter.handleFullscreenButton();

        $(presenter.modelViewer).on('load', function () {
            if (presenter.configuration.scale !== undefined &amp;&amp; presenter.configuration.scale !== "") presenter.setScale(presenter.configuration.scale);
        });

        presenter.wasInitiated = true;
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeModelWithEnableFullscreen(model);
        upgradedModel = presenter.upgradeModelWithModelIOS(upgradedModel);
        return presenter.upgradeModelWithLangAttribute(upgradedModel);
    };

    presenter.upgradeModelWithEnableFullscreen = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["enableFullscreen"]) {
            upgradedModel["enableFullscreen"] = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeModelWithModelIOS = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model.hasOwnProperty('modelIOS')) {
            upgradedModel['modelIOS'] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeModelWithLangAttribute = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model.hasOwnProperty('langAttribute')) {
            upgradedModel['langAttribute'] = "";
        }

        return upgradedModel;
    };

    presenter.validateModel = function (model) {
        let isValid = true;
        let additionalAttributes;
        const isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        const isAutoRotate = ModelValidationUtils.validateBoolean(model["autoRotate"]);
        const areLabelsEnabled = ModelValidationUtils.validateBoolean(model["labelsEnabled"]);
        const isInteractionPrompt = ModelValidationUtils.validateBoolean(model["interactionPrompt"]);
        const environmentImage = model["environmentImage"] === "" ? "neutral" : model["environmentImage"];
        try {
            const attributes = model["attributes"];
            if (attributes &amp;&amp; attributes.trim() !== "") {
                additionalAttributes = JSON.parse(attributes.trim());
            }
        } catch (e) {
            isValid = false;
        }

        return {
            isValid: isValid,
            isVisible: isVisible,
            addonID: model["ID"],
            model: model["model"],
            modelIOS: model["modelIOS"],
            poster: model["poster"],
            annotations: model["annotations"].trim(),
            environmentImage: environmentImage,
            skyboxImage: model["skyboxImage"],
            shadowSoftness: model["shadowSoftness"],
            shadowIntensity: model["shadowIntensity"],
            autoRotate: isAutoRotate,
            scale: model["scale"],
            labelsEnabledOnStart: areLabelsEnabled,
            altText: model["altText"],
            additionalAttributes: additionalAttributes,
            copyInfo: model["copyInfo"],
            interactionPrompt: isInteractionPrompt,
            enableFullscreen: ModelValidationUtils.validateBoolean(model["enableFullscreen"]),
            langTag: model["langAttribute"],
        };
    };

    presenter.extendView = function (isPreview) {
        presenter.modelViewer = document.createElement("model-viewer");
        presenter.handleAttributes(isPreview);

        let parent = presenter.view;
        if (presenter.configuration.enableFullscreen) {
            parent = document.createElement("div");
            parent.classList.add("modelViewerWrapper");
            presenter.wrapper = parent;
            presenter.view.append(parent);
        }
        parent.append(presenter.modelViewer);

        presenter.copyMessage = document.createElement("div");
        presenter.copyMessage.classList.add("copyMessage");
        parent.append(presenter.copyMessage);

        const buttonsContainer = document.createElement("div");
        buttonsContainer.classList.add("buttonsContainer");
        parent.append(buttonsContainer);

        presenter.labelsButton = document.createElement("div");
        presenter.labelsButton.classList.add("labelsButton");
        buttonsContainer.append(presenter.labelsButton);

        presenter.copyButton = document.createElement("div");
        presenter.copyButton.classList.add("copyButton");
        buttonsContainer.append(presenter.copyButton);

        presenter.fullscreenButton = document.createElement("div");
        presenter.fullscreenButton.classList.add("fullscreenButton");
        buttonsContainer.append(presenter.fullscreenButton);
    };

    presenter.handleAttributes = function (isPreview) {
        presenter.modelViewer.setAttribute("src", presenter.configuration.model);
        presenter.modelViewer.setAttribute("poster", presenter.configuration.poster);
        presenter.modelViewer.setAttribute("skybox-image", presenter.configuration.skyboxImage);
        presenter.modelViewer.setAttribute("environment-image", presenter.configuration.environmentImage);
        presenter.modelViewer.setAttribute("shadow-intensity", presenter.configuration.shadowIntensity);
        presenter.modelViewer.setAttribute("shadow-softness", presenter.configuration.shadowSoftness);
        presenter.modelViewer.setAttribute("alt", presenter.configuration.altText);
        presenter.modelViewer.setAttribute("ar", "");
        presenter.modelViewer.setAttribute("camera-controls", "");
        presenter.modelViewer.setAttribute("touch-action", "pan-y");
        !isPreview &amp;&amp; presenter.modelViewer.setAttribute("vr", "");

        if (presenter.isMobileIOS()) {
            presenter.modelViewer.setAttribute("xr-environment", "");
            presenter.modelViewer.setAttribute("ar-mode", "webxr scene-viewer quick-look fallback");
            presenter.configuration.modelIOS &amp;&amp;
                presenter.modelViewer.setAttribute("ios-src", presenter.configuration.modelIOS);
        }
        presenter.addAttributesFromModel();
    };

    presenter.addAttributesFromModel = function () {
        if (!presenter.configuration.additionalAttributes) {
            return;
        }

        const attributes = presenter.configuration.additionalAttributes;

        Object.keys(attributes).forEach(key =&gt; {
            $(presenter.modelViewer).attr(key, attributes[key]);
        });
    };

    presenter.setAnnotations = function () {
        $(presenter.modelViewer).append(presenter.configuration.annotations);
        presenter.configuration.labelsEnabledOnStart ? presenter.showAnnotations() : presenter.hideAnnotations();
        presenter.areAnnotationsVisibleOnStart = presenter.annotationsVisibility === 'visible';
    };

    presenter.setAutoRotation = function () {
        if (presenter.configuration.autoRotate) {
            $(presenter.modelViewer).attr("auto-rotate", true);
        }
    };

    presenter.setInteractionPrompt = function () {
        presenter.configuration.interactionPrompt ? $(presenter.modelViewer).attr("interaction-prompt", "auto") : $(presenter.modelViewer).attr("interaction-prompt", "none");
    };

    presenter.handleButtons = function () {
        presenter.handleDisplayingButtons();

        presenter.configuration.labelsEnabledOnStart ? $(presenter.labelsButton).addClass("labelsButton-selected") : $(presenter.labelsButton).removeClass("labelsButton-selected");

        $(presenter.labelsButton).on("click", function (e) {
            $(presenter.labelsButton).toggleClass("labelsButton-selected");
            $(presenter.labelsButton).hasClass("labelsButton-selected") ? presenter.showAnnotations() : presenter.hideAnnotations();
        });
    };

    presenter.handleCopyright = function () {
        const copyText = '&lt;div class="copyContainer"&gt;' + presenter.configuration.copyInfo + '&lt;/div&gt;';
        $(presenter.copyMessage).append(copyText);

        presenter.copyContainer = $(presenter.copyMessage).find(".copyContainer").get(0);
        const link = presenter.copyContainer.querySelector('a');
        if (link) {
            link.addEventListener('click', function (event) {
                event.preventDefault();
                window.open(link.getAttribute('href'), '_blank');
            });
        }

        $(presenter.copyButton).on("click", function (e) {
            $(presenter.copyButton).toggleClass("copyButton-selected");
            $(presenter.copyMessage).toggleClass("copyMessage-visible");
        });
    };

    presenter.handleFullscreenButton = function () {
        $(presenter.fullscreenButton).on("click", function (e) {
            if (presenter.fullscreen.isOn) {
                presenter.exitFullscreen();
            } else {
                presenter.requestFullscreen();
            }
        });
    };

    presenter.handleDisplayingButtons = function () {
        if (presenter.configuration.annotations === "") {
            $(presenter.labelsButton).addClass("hidden");
        }

        if (presenter.configuration.copyInfo === "") {
            $(presenter.copyButton).addClass("hidden");
        }

        if (!isFullscreenEnabled()) {
            $(presenter.fullscreenButton).addClass("hidden");
        }
    };

    presenter.disableEvents = function () {
        $(presenter.modelViewer).css('pointer-events', 'none');
    };

    presenter.setScale = function (scale) {
        presenter.modelViewer.scale = scale + " " + scale + " " + scale;
    };

    presenter.showAnnotations = function () {
        if (!presenter.configuration.isVisible) { return; }
        presenter.setAnnotationsVisibility("visible");
        $(presenter.labelsButton).addClass("labelsButton-selected");
    };

    presenter.hideAnnotations = function () {
        presenter.setAnnotationsVisibility("hidden");
        $(presenter.labelsButton).removeClass("labelsButton-selected");
    };

    presenter.setAnnotationsVisibility = function (value) {
        $(presenter.modelViewer).find(".Hotspot").css("visibility", value);
        presenter.annotationsVisibility = value;
    };

    presenter.getAnnotationsVisibility = function () {
        return presenter.annotationsVisibility;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'showAnnotations': presenter.showAnnotations,
            'hideAnnotations': presenter.hideAnnotations,
            'getAnnotationsVisibility': presenter.getAnnotationsVisibility,
            'setScale': presenter.setScale,
            'requestFullscreen': presenter.requestFullscreen,
            'exitFullscreen': presenter.exitFullscreen,
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setShowErrorsMode = function () { };

    presenter.setWorkMode = function () { };

    presenter.reset = function () {
        if (presenter.isVisibleOnStart !== presenter.configuration.isVisible) {
            presenter.isVisibleOnStart ? presenter.show() : presenter.hide();
        }

        // set annotations visibility to default
        presenter.areAnnotationsVisibleOnStart ? presenter.showAnnotations() : presenter.hideAnnotations();

        if ($(presenter.copyButton).hasClass("copyButton-selected")) {
            $(presenter.copyButton).removeClass("copyButton-selected");
            $(presenter.copyMessage).removeClass("copyMessage-visible");
        }

        // handle animation
        presenter.setAnimationToDefault();
    };

    presenter.getErrorCount = function () {
        return 0;
    };

    presenter.getMaxScore = function () {
        return 0;
    };

    presenter.getScore = function () {
        return 0;
    };

    presenter.changeAnimation = function (animation) {
        $(presenter.modelViewer).attr("animation-name", animation);
    };

    presenter.play = function (repetitions, pingpong) {
        presenter.modelViewer.play(repetitions, pingpong);
    };

    presenter.pause = function () {
        presenter.modelViewer.pause();
    };

    presenter.listAnimations = function () {
        return presenter.modelViewer.availableAnimations;
    };

    presenter.jumpTo = function (time) {
        presenter.modelViewer.currentTime = time;
    };

    presenter.isPaused = function () {
        return presenter.modelViewer.paused;
    };

    presenter.animationDuration = function () {
        return presenter.modelViewer.duration;
    };

    presenter.changeSpeed = function (speed) {
        presenter.modelViewer.timeScale = speed;
    };

    presenter.userPrompt = function (value) {
        $(presenter.modelViewer).attr("interaction-prompt", value);
    };

    presenter.currentTime = function () {
        return presenter.modelViewer.currentTime;
    };

    presenter.getState = function () {
        return JSON.stringify({
            annotationsVisibility: presenter.annotationsVisibility,
            isVisible: presenter.configuration.isVisible,
            isPlaying: !presenter.isPaused(),
            selectedAnimation: $(presenter.modelViewer).attr("animation-name"),
            animationTime: presenter.modelViewer.currentTime
        });
    };

    presenter.setState = function (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        var state = JSON.parse(stateString);
        presenter.annotationsVisibility = state.annotationsVisibility;

        presenter.annotationsVisibility === "visible" ? presenter.showAnnotations() : presenter.hideAnnotations();

        if (state.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.handleAnimationState(state);
    };

    presenter.handleAnimationState = function (state) {
        $(presenter.modelViewer).attr("animation-name", state.selectedAnimation);

        setTimeout(function () {
            state.isPlaying ? presenter.play() : presenter.pause();
            presenter.jumpTo(state.animationTime);
        }, 100);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
        if (presenter.annotationsVisibility === "visible") {
            presenter.showAnnotations();
        }
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
        $(presenter.modelViewer).find(".Hotspot").css("visibility", "hidden");
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.setAnimationToDefault = function () {
        const animations = presenter.listAnimations();
        const userSelectedAnimations = presenter.getUserSelectedAnimation();
        const isAutoplay = presenter.getAutoplayStatus();
        if (userSelectedAnimations) {
            presenter.changeAnimation(userSelectedAnimations);
        } else if (animations.length) {
            presenter.changeAnimation(animations[0]);
        }

        presenter.jumpTo(0);

        if (isAutoplay) {
            presenter.modelViewer.play();
        } else {
            presenter.pause();
        }
    };

    presenter.getUserSelectedAnimation = function () {
        if (presenter.configuration.additionalAttributes &amp;&amp; presenter.configuration.additionalAttributes.hasOwnProperty('animation-name')) {
            return presenter.configuration.additionalAttributes['animation-name'];
        }
    };

    presenter.getAutoplayStatus = function () {
        if (presenter.configuration.additionalAttributes &amp;&amp; presenter.configuration.additionalAttributes.hasOwnProperty('autoplay')) {
            return presenter.configuration.additionalAttributes['autoplay'];
        }

        return false;
    };

    function showErrorMessage() {
        presenter.$view.addClass('error');
        presenter.$view.html("Additional Attributes must be in JSON format.");
    }

    presenter.addHandlers = function () {
        $(document).on(
            'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange',
            presenter.fullscreenChangedEventReceived
        );
        MutationObserverService.createDestroyObserver(
            presenter.configuration.addonID,
            presenter.destroy,
            presenter.view
        );
        MutationObserverService.setObserver();
    };

    presenter.requestFullscreen = function () {
        if (!isFullscreenEnabled() || presenter.fullscreen.isOn) {
            return;
        }
        presenter.fullscreen.requestMethod.call(presenter.wrapper);
        setUpToFullscreenMode();
    };

    function setUpToFullscreenMode() {
        presenter.fullscreen.isOn = true;
        presenter.playerController.setAbleChangeLayout(false);
        presenter.wrapper.classList.add("modelViewerFullscreen");
    }

    presenter.exitFullscreen = function () {
        if (!isFullscreenEnabled() || !presenter.fullscreen.isOn) {
            return;
        }
        presenter.fullscreen.exitMethod.call(document);
        cleanAfterFullscreenMode();
    };

    presenter.fullscreenChangedEventReceived = function (event) {
        if (!isFullscreenInDocument() &amp;&amp; presenter.fullscreen.isOn) {
            cleanAfterFullscreenMode();
        }
    };

    function cleanAfterFullscreenMode() {
        presenter.fullscreen.isOn = false;
        presenter.playerController.setAbleChangeLayout(true);
        presenter.wrapper.classList.remove("modelViewerFullscreen");
    }

    function isFullscreenEnabled() {
        return presenter.configuration.enableFullscreen &amp;&amp; presenter.fullscreen.hasSupport;
    }

    function checkFullscreenSupport() {
        presenter.fullscreen = {
            isOn: false,
            hasSupport: false,
            requestMethod: null,
            exitMethod: null,
        };

        presenter.fullscreen.hasSupport = !!(
            document.fullscreenEnabled ||
            document.webkitFullscreenEnabled ||
            document.mozFullScreenEnabled ||
            document.msFullscreenEnabled
        );

        if (!isFullscreenEnabled()) {
            return;
        }

        presenter.fullscreen.requestMethod = (
            presenter.wrapper.requestFullscreen ||
            presenter.wrapper.mozRequestFullScreen ||
            presenter.wrapper.msRequestFullscreen ||
            presenter.wrapper.webkitRequestFullScreen ||
            presenter.wrapper.webkitEnterFullscreen ||
            null
        );

        presenter.fullscreen.exitMethod = (
            document.exitFullscreen ||
            document.mozCancelFullScreen ||
            document.msExitFullscreen ||
            document.webkitExitFullscreen ||
            null
        );
    }

    function isFullscreenInDocument() {
        return !!(
            document.fullscreenElement ||
            document.mozFullScreenElement ||
            document.webkitFullscreenElement ||
            document.msFullscreenElement ||
            document.webkitCurrentFullScreenElement ||
            document.fullscreen ||
            document.webkitIsFullScreen ||
            document.mozFullScreen
        );
    }

    presenter.isMobileIOS = function () {
        return window.MobileUtils.isSafariMobile(navigator.userAgent);
    }

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        presenter.exitFullscreen();
        $(document).off(
            'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange',
            presenter.fullscreenChangedEventReceived
        );
    };

    presenter.keyboardController = function(keyCode, isShift, event) {
        event.preventDefault();
        if (keyCode === window.KeyboardControllerKeys.ENTER) {
            presenter.readAltText();
        }
    };

    presenter.readAltText = function () {
        presenter.speak([window.TTSUtils.getTextVoiceObject(presenter.configuration.altText, presenter.configuration.langTag)]);
    }

    presenter.getTextToSpeechOrNull = function () {
        if (presenter.playerController) {
            return presenter.playerController.getModule('Text_To_Speech1');
        }
        return null;
    };

    presenter.speak = function (data) {
        const tts = presenter.getTextToSpeechOrNull();
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function (isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.isEnterable = function(){ return false;};

    presenter.isSelectable = function(isWCAGOn) {
        return isWCAGOn;
	}

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="MultiAudio" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Files" nameLabel="MultiAudio_property_files" type="list">
			<property name="ID" nameLabel="MultiAudio_property_id" type="string"/>
			<property name="Mp3" nameLabel="MultiAudio_property_mp3" type="audio"/>
			<property name="Ogg" nameLabel="MultiAudio_property_ogg" type="audio"/>
			<property name="Enable loop" nameLabel="MultiAudio_property_enable_loop" type="boolean"/>
			<property isLocalized="true" name="Text" nameLabel="MultiAudio_property_text" type="html"/>
		</property>
		<property name="Interface" nameLabel="MultiAudio_property_interface" type="{None, Default controls, Display time, Draggable items}"/>
		<property isLocalized="true" name="Narration" nameLabel="MultiAudio_property_narration" type="narration"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="MultiAudio_property_lang_attribute" type="string"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="MultiAudio_property_property_speech_texts" type="staticlist">

			<property name="Selected" nameLabel="MultiAudio_property_selected" type="staticrow">
				<property name="Selected" nameLabel="MultiAudio_property_selected" type="string"/>
			</property>

			<property name="Deselected" nameLabel="MultiAudio_property_deselected" type="staticrow">
				<property name="Deselected" nameLabel="MultiAudio_property_deselected" type="string"/>
			</property>

			<property name="Empty" nameLabel="MultiAudio_property_empty" type="staticrow">
				<property name="Empty" nameLabel="MultiAudio_property_empty" type="string"/>
			</property>

		</property>
	</model>
<css>.wrapper-addon-audio audio {
    bottom: 0;
    position: absolute;
}

.wrapper-addon-audio {
    height: 100%;
    position: relative;
}

.multiaudio-item-wrapper {
    width: 110px;
    height: 45px;
    display: block;
    background-color: #3792cb;
    padding: 5px;
    margin: 2px;
}

.multiaudio-item-wrapper.multiaudio-item-has-text {
    min-width: 110px;
    min-height: 45px;
    width: fit-content;
    height: auto;
    display:flex;
    flex-wrap: wrap;
    padding: 3px;
}

.multiaudio-selected {
    border: 1px solid orange;
    z-index: 100;
}

.multiaudio-item-wrapper .multiaudio-item-grab-area {
    background-color: rgba(0,0,0,0.15);
    width: 32px;
    height: 44px;
    margin-right: 5px;
    display:inline-block;
    cursor: pointer;
}

.multiaudio-item-wrapper.multiaudio-item-has-text  .multiaudio-item-grab-area {
    background-color: white;
    min-width: 32px;
    min-height: 44px;
    width: auto;
    height: 100%;
    margin: 3px;
}

.multiaudio-item-wrapper .multiaudio-item-button {
    width: 71px;
    height: 44px;
    display:inline-block;
    background: white;
}

.multiaudio-item-wrapper.multiaudio-item-has-text .multiaudio-item-button {
    margin: 3px;
}

.multiaudio-item-wrapper .multiaudio-item-icon {
    width: 100%;
    height: 100%;
    display:block;
    background: #3792cb;
    mask: url('resources/volume_up-24px.svg') no-repeat center;
    -webkit-mask: url('resources/volume_up-24px.svg') no-repeat center;
}

.multiaudio-item-wrapper.playing .multiaudio-item-icon {
    width: 100%;
    height: 100%;
    display:block;
    background: #3792cb;
    mask: url('resources/volume_off-24px.svg') no-repeat center;
    -webkit-mask: url('resources/volume_off-24px.svg') no-repeat center;
}
</css><view>&lt;div class="wrapper-addon-audio"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="wrapper-addon-audio"&gt;

&lt;/div&gt;
</preview><presenter>function AddonMultiAudio_create(){
    var presenter = function(){};
    var AUDIO_FILES_MISSING = "This addon needs at least 1 audio file.";
    var eventBus;
    var currentTimeAlreadySent;
    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);
    presenter.currentAudio = 0;
    presenter.audio = {};
    presenter.files = [];
    presenter.visible = true;
    presenter.defaultVisibility = true;
    presenter.globalView = null;
    presenter.globalModel = null;
    presenter.playerController = null;
    presenter.addonID = null;
    presenter.type = 'multiaudio';
    presenter.draggableItems = {};
    presenter.isWCAGOn = false;
    presenter.selectedItemID = '';
    presenter.currentDraggableItemID = '';

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    function getTextVoiceObject(text, lang) {return window.TTSUtils.getTextVoiceObject(text, lang);}
    
    presenter.onEventReceived = function(eventName, eventData) {
        if (eventName == "ValueChanged") {
            if (eventData.value == 'dropdownClicked' &amp;&amp; !presenter.audio.playing &amp;&amp; !isTemporarilyPaused()) {
                this.audio.load();
            }
        }else if (eventName == "ItemConsumed") {
            if (presenter.globalModel["Interface"] == "Draggable items") {
                var itemID = getItemIdFromEvent(eventData.item);
                if (itemID != null) {
                    removeDraggableItem(itemID);
                }
            }
        } else if (eventName == "ItemReturned") {
            if (presenter.globalModel["Interface"] == "Draggable items") {
                var itemID = getItemIdFromEvent(eventData.item);
                if (itemID != null) {
                    createDraggableItem(itemID);
                }
            }
        } else if (eventName == "itemDragged") {
            if (presenter.globalModel["Interface"] == "Draggable items") {
                var itemID = getItemIdFromEvent(eventData.item);
                presenter.fireSelectedDraggableEvent(itemID);
                hideDraggableItem(itemID);
            }
        } else if (eventName == "itemStopped") {
            if (presenter.globalModel["Interface"] == "Draggable items") {
                var itemID = getItemIdFromEvent(eventData.item);
                if (itemID != null) {
                    showDraggableItem(itemID);
                }
            }
        } else if (eventName == "ItemSelected" &amp;&amp; eventData.item !== null) { // when ImageSource deselects item, then item is null
            var itemID = getItemIdFromEvent(eventData.item);
            this.applySelectedClass(itemID);
        }
    };

    function isTemporarilyPaused() {
        return (presenter.audio.paused
            &amp;&amp; presenter.audio.readyState &gt; 2
            &amp;&amp; presenter.audio.currentTime &gt; 0
            &amp;&amp; !presenter.audio.ended
        );
    }

    function getItemIdFromEvent(eventDataItem) {
        var addonAndItemIds = eventDataItem.split('-');
        if (addonAndItemIds.length != 2) return null;
        if (addonAndItemIds[0] != presenter.addonID) return null;
        return addonAndItemIds[1];
    }

    presenter.applySelectedClass = function(itemID) {
        if (presenter.globalModel["Interface"] != "Draggable items") return;
        var keys = Object.keys(presenter.draggableItems);
        for (var i = 0; i &lt; keys.length; i++) {
            var key = keys[i];
            var item = presenter.draggableItems[key];
            if (key == itemID) {
                item.addClass('multiaudio-selected');
            } else {
                item.removeClass('multiaudio-selected');
            }
        }
    }

    function getEventObject(_item, _value, _score) {
    	return {
            source : presenter.addonID,
            item : _item + '',
            value : _value + '',
            score : _score + ''
        };
    }

    function deferredQueueDecoratorChecker () {
        return presenter.isLoaded;
    }
    
    presenter.createEventData = function (data) {
    	return getEventObject(data.currentItem, data.currentTime, '');
    };

    presenter.createOnEndEventData = function (data) {
        return getEventObject(data.currentItem, 'end', '');
    };
    
    presenter.createOnPlayingEventData = function (data) {
        return getEventObject(data.currentItem, 'playing', '');
    };

    presenter.createOnPauseEventData = function (data) {
        return getEventObject(data.currentItem, 'pause', '');
    };
    
    presenter.sendEventAndSetCurrentTimeAlreadySent = function (eventData, currentTime) {
        eventBus.sendEvent('ValueChanged', eventData);
        currentTimeAlreadySent = currentTime;
    };

    presenter.getAudioCurrentTime = function () {
        return this.audio.currentTime;
    };

    presenter.onTimeUpdateSendEventCallback = function() {

        var ua = navigator.userAgent;
        if( ua.indexOf("Android") &gt;= 0 )
        {
            var androidversion = parseFloat(ua.slice(ua.indexOf("Android")+8));
            if (androidversion == 4.4)
            {
                var duration = parseInt(presenter.audio.duration, 10);
                duration = isNaN(duration) ? 0 : duration;
                var currentTime2 = parseInt(presenter.audio.currentTime, 10);

                if(duration == currentTime2){
                    presenter.sendOnEndEvent();
                }
            }
        }

        var currentTime = presenter.formatTime(presenter.getAudioCurrentTime());
        var currentItem = presenter.currentAudio+1;
        if (currentTime !== currentTimeAlreadySent) { // to prevent duplicated value
            var eventData = presenter.createEventData({'currentTime' : currentTime, 'currentItem': currentItem});
            presenter.sendEventAndSetCurrentTimeAlreadySent(eventData, currentTime);
        }
    };
    
    presenter.addAttributeLoop = function(audio) {
        $(audio).on("ended", function() {
            this.currentTime = 0;
            this.play();
        });
    };

    presenter.prepareAudio = function(){
        this.audio = document.createElement("audio");
        var audioWrapper = presenter.globalView.find(".wrapper-addon-audio");
        audioWrapper.html("");
        audioWrapper.append(this.audio);
        return audioWrapper;
    };
    
    presenter.sendOnEndEvent = function () {
        var currentItem = presenter.currentAudio+1;
        var eventData = presenter.createOnEndEventData({'currentItem': currentItem});
        eventBus.sendEvent('ValueChanged', eventData);
    };
    
    presenter.sendOnPlayingEvent = function () {
        var currentItem = presenter.currentAudio+1;
        var eventData = presenter.createOnPlayingEventData({'currentItem': currentItem});
            eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.sendOnPausedEvent = function () {
        var currentItem = presenter.currentAudio+1;
        var eventData = presenter.createOnPauseEventData({'currentItem': currentItem});
            eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.playingEventSent = false;
    presenter.createView = function(view, model){
        var interfaceType = model["Interface"];
        var audioWrapper = this.prepareAudio();
        this.audio.addEventListener('timeupdate', function() {
            presenter.onTimeUpdateSendEventCallback();

            var ua = navigator.userAgent;
            if( ua.indexOf("Android") &gt;= 0 )
            {
                var androidversion = parseFloat(ua.slice(ua.indexOf("Android")+8));
                if (androidversion == 5)
                {
                    if(presenter.audio.currentTime &gt; 0 &amp;&amp; !presenter.playingEventSent){
                        presenter.sendOnPlayingEvent();
                        presenter.playingEventSent = true;
                    }
                }
            }
        }, false);
        this.audio.addEventListener('playing', function () {
            var ua = navigator.userAgent;
            if( ua.indexOf("Android") &gt;= 0 ){
                var androidversion = parseFloat(ua.slice(ua.indexOf("Android")+8));
                if (androidversion == 5){
                    //do nothing
                }else{
                    presenter.sendOnPlayingEvent();
                }
            }else{
                presenter.sendOnPlayingEvent();
            }
        }, false);
        this.audio.addEventListener('play', function () {
        }, false);
        this.audio.addEventListener('click', function(e) {
            e.stopPropagation();
        }, false);
        this.audio.addEventListener('ended', function() {
            presenter.stop();
            presenter.sendOnEndEvent();
            presenter.playingEventSent = false;
        }, false);

        this.audio.addEventListener('pause', function() {
                if (interfaceType == "Default controls") {
                    presenter.sendOnPausedEvent();
                }
            }, false);

        if (!presenter.isLoaded) {
            this.audio.addEventListener("loadeddata", function() {
                presenter.isLoaded = true;

                deferredSyncQueue.resolve();
            });
        }

        switch(interfaceType) {
            case "Default controls":
                $(this.audio).attr("controls", "controls").attr("preload", "auto");
                break;
            case "Display time":
                this.createCurrentAndDuration(audioWrapper);
                this.audio.addEventListener('loadeddata', onLoadedMetadataCallback, false);
                this.audio.addEventListener('timeupdate', onTimeUpdateCallback, false);
                break;
            case "Draggable items":
                presenter.createDraggableItems(model['Files']);
                break;
        }

        Object.defineProperty(presenter.audio, 'playing', {
            get: function () {
                return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
            }
        });
    };

    presenter.createDraggableItems = function(filesModel) {
        for (var i=0; i &lt; filesModel.length; i++) {
            createDraggableItem(filesModel[i].ID);
        }
        if (presenter.selectedItemID.length &gt; 0) {
            presenter.applySelectedClass(presenter.selectedItemID);
        }
    };

    function createDraggableItem (itemID) {
        if (itemID in presenter.draggableItems) return;

        var $el = $('&lt;div&gt;&lt;/div&gt;');
            $el.attr('data-audio-id', itemID);
            $el.attr('data-addon-id', presenter.addonID);
            $el.addClass('multiaudio-item-wrapper');

            var $grab = $('&lt;div&gt;&lt;/div&gt;');
            $grab.addClass('multiaudio-item-grab-area');
            $el.append($grab);

            var itemText = presenter.getTextFromFileID(itemID);
            if (presenter.playerController) {
                itemText = presenter.playerController.getTextParser().parseAltTexts(itemText);
            } else {
                itemText = window.TTSUtils.parsePreviewAltText(itemText);
            }
            if ($("&lt;span&gt;" + itemText + "&lt;/span&gt;").text().length &gt; 0) {
                var $text = $('&lt;span&gt;&lt;/span&gt;');
                $text.addClass('multiaudio-item-text');
                $text.html(itemText);
                $grab.append($text);
                $el.addClass("multiaudio-item-has-text");
            }

            $grab.click(function(){presenter.handleGrabAreaClick(itemID)});

            var $button = $('&lt;div&gt;&lt;/div&gt;');
            $button.addClass('multiaudio-item-button');
            $el.append($button);

            var $icon = $('&lt;div&gt;&lt;/div&gt;');
            $icon.addClass('multiaudio-item-icon');
            $button.append($icon);

            $button.click(draggableItemButtonClickHandler);

            $el.draggable({
                revert: true,
                helper: "original",
                handle: ".multiaudio-item-grab-area",
                start : function(event, ui) {
                    presenter.fireSelectedDraggableEvent(itemID);
                    if (presenter.draggableItems[itemID].hasClass('playing')) {
                        presenter.stop();
                        presenter.draggableItems[itemID].removeClass('playing');
                    }
                },
                drag : function(event, ui) {
                    ui.position.left = ui.position.left / getScale().X;
                    ui.position.top = ui.position.top / getScale().Y;
                }
            });

            presenter.globalView.find(".wrapper-addon-audio").append($el);
            presenter.draggableItems[itemID] = $el;
    }

    function getScale() {
        if (presenter.playerController) {
            const scaleInformation = presenter.playerController.getScaleInformation();
            if (scaleInformation.baseScaleX !== 1.0 ||
                scaleInformation.baseScaleY !== 1.0 ||
                scaleInformation.scaleX !== 1.0 ||
                scaleInformation.scaleY !== 1.0
            ) {
                return {X: scaleInformation.scaleX, Y: scaleInformation.scaleY};
            }
        }

        const $content = $("#content");
        if ($content.size() &gt; 0) {
            const contentElem = $content[0];
            const scaleX = contentElem.getBoundingClientRect().width / contentElem.offsetWidth;
            const scaleY = contentElem.getBoundingClientRect().height / contentElem.offsetHeight;
            return {X: scaleX, Y: scaleY};
        } else {
            return {X: 1.0, Y: 1.0};
        }
    }

    presenter.getTextFromFileID = function(itemID) {
        // This method is used by the multiplegap addon while creating the draggable audio widgets
        var item = null;
        for (var i = 0; i &lt; presenter.globalModel.Files.length; i++) {
            var tmpItem = presenter.globalModel.Files[i];
            if (tmpItem.ID === itemID) {
                item = tmpItem;
            }
        }
        if (item === null) return null;
        return item["Text"];
    }

    function removeDraggableItem(itemID) {
        if (!(itemID in presenter.draggableItems)) return;

        presenter.draggableItems[itemID].remove();
        delete presenter.draggableItems[itemID];
        if (presenter.selectedItemID == itemID) {
            presenter.selectedItemID = '';
        }
    }

    function removeDraggableItems() {
        presenter.globalView.find('.multiaudio-item-wrapper').remove();
        presenter.draggableItems = {};
        presenter.selectedItemID = '';
    }

    function hideDraggableItem(itemID) {
        if (itemID in presenter.draggableItems) {
            presenter.draggableItems[itemID].css('display','none');
        }
    }

    function showDraggableItem(itemID) {
        if (itemID in presenter.draggableItems) {
            presenter.draggableItems[itemID].css('display','');
        }
    }

    // itemID is optional, if left empty a "deselecting" event will be fired
    presenter.fireSelectedDraggableEvent = function(itemID) {
        var eventData = {
            source : presenter.addonID,
            type : 'audio',
            item : "",
            value : ""
        };
        if (itemID != null) {
            eventData.item = presenter.addonID + '-' + itemID;
            eventData.value = itemID + '';
        }
        eventBus.sendEvent('ItemSelected', eventData);
    };

    presenter.handleGrabAreaClick = function(itemID) {
        if (presenter.draggableItems[itemID].hasClass('ui-draggable-dragging')) return;
        if (presenter.draggableItems[itemID].hasClass('multiaudio-selected')) {
            presenter.fireSelectedDraggableEvent();
            presenter.selectedItemID = '';
            readDeselected();
        } else {
            presenter.fireSelectedDraggableEvent(itemID);
            presenter.selectedItemID = itemID;
            readSelected();
        }
    };

    function draggableItemButtonClickHandler (event) {
        var $parent = $(event.currentTarget).parent();
        var itemID = $parent.attr('data-audio-id');
        if ($parent.hasClass('playing')) {
            presenter.stop();
            presenter.jumpToID(itemID);
        } else {
            $parent.addClass('playing');
            presenter.jumpToID(itemID);
            presenter.play();

        }
    }

    presenter.createCurrentAndDuration = function(audioWrapper) {
        var currentTime = document.createElement("span");
        var durationTime = document.createElement("span");
        $(currentTime).attr("id", "currentTime");
        $(durationTime).attr("id", "durationTime");
        audioWrapper.html(currentTime).append(durationTime);
    };

    presenter.formatTime = function(seconds) {
        var minutes = Math.floor(seconds / 60);
        minutes = (minutes &gt;= 10) ? minutes : "0" + minutes;
        seconds = Math.floor(seconds % 60);
        seconds = (seconds &gt;= 10) ? seconds : "0" + seconds;
        return minutes + ":" + seconds;
    };

    function onLoadedMetadataCallback() {
        var duration = parseInt(presenter.audio.duration, 10);
        duration = isNaN(duration) ? 0 : duration;
        presenter.displayTimer(0, duration);
    }

    function onTimeUpdateCallback() {
        var duration = parseInt(presenter.audio.duration, 10);
        duration = isNaN(duration) ? 0 : duration;
        var currentTime = presenter.audio.currentTime;
        presenter.displayTimer(currentTime, duration);
    }

    presenter.displayTimer = function(current, duration) {
        presenter.globalView.find('#currentTime').html(presenter.formatTime(current) + ' / ');
        presenter.globalView.find('#durationTime').html(presenter.formatTime(duration));
    };

    presenter.loadFiles = function(audio, model){
        this.files = model["Files"];
        var oggFile = this.files[this.currentAudio]["Ogg"];
        var mp3File = this.files[this.currentAudio]["Mp3"];
        var loop = !!(this.files[this.currentAudio]["Enable loop"] == "True");
        var canPlayMp3 = false;
        var canPlayOgg = false;

        var validated = this.validateFiles(this.files[this.currentAudio]);

        if (!validated) {
            this.globalView.find(".wrapper-addon-audio").html(AUDIO_FILES_MISSING);
        }

        if (loop) {
            presenter.addAttributeLoop(audio);
        }

        if(audio.canPlayType) {
            canPlayMp3 = !!audio.canPlayType &amp;&amp; "" != audio.canPlayType('audio/mpeg');
            canPlayOgg = !!audio.canPlayType &amp;&amp; "" != audio.canPlayType('audio/ogg; codecs="vorbis"');
            if(canPlayMp3){
                $(audio).attr("src", mp3File);
            } else if (canPlayOgg) {
                $(audio).attr("src", oggFile);
            }
        } else {
            $(audio).append("Your browser doesn't support audio.");
        }

        audio.load();

    };

    function upgradeModel(model) {
        var upgradedModel = upgradeFileText(model);
        upgradedModel = upgradeTextToSpeechSupport(upgradedModel);
        return upgradedModel;
    }

    function upgradeFileText(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);
        for (var i = 0; i &lt; upgradedModel["Files"].length; i++) {
            if (upgradedModel["Files"][i]["Text"] === undefined) {
                upgradedModel["Files"][i]["Text"] = "";
            }
        }
        return upgradedModel;
    }

    function upgradeTextToSpeechSupport(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel['speechTexts'] === undefined) {
            upgradedModel['speechTexts'] = {
                Selected: {Selected: "Selected"},
                Deselected: {Deselected: "Deselected"},
                Empty: {Empty: "Empty"}
            };
        }

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] = "";
        }

        return upgradedModel;
    }

    function getSpeechTextProperty (rawValue, defaultValue) {
            var value = rawValue.trim();

            if (value === undefined || value === null || value === '') {
                return defaultValue;
            }

            return value;
        }

    presenter.getSpeechTexts = function(speechTextsModel) {
        var speechTexts = {
            selected:  'Selected',
            deselected: 'Deselected',
            empty: 'Empty'
        };

        if (!speechTextsModel) {
            return speechTexts;
        }

        speechTexts = {
            selected:        getSpeechTextProperty(speechTextsModel['Selected']['Selected'], speechTexts.selected),
            deselected:        getSpeechTextProperty(speechTextsModel['Deselected']['Deselected'], speechTexts.deselected),
            empty:        getSpeechTextProperty(speechTextsModel['Empty']['Empty'], speechTexts.empty)
        };

        return speechTexts;
    };

    presenter.run = function(view, model){
        this.initialize(view, model, false);
        eventBus = presenter.playerController.getEventBus();
        presenter.addonID = model.ID;
        eventBus.addEventListener('ValueChanged', this);
        eventBus.addEventListener('ItemSelected', this);
        eventBus.addEventListener('ItemConsumed', this);
        eventBus.addEventListener('ItemReturned', this);
        eventBus.addEventListener('itemStopped', this);
        eventBus.addEventListener('itemDragged', this);
    };

    presenter.createPreview = function(view, model){
        this.initialize(view, model, true);
    };

    presenter.initialize = function(view, model, isPreview) {
        var upgradedModel = upgradeModel(model);
        this.globalModel = upgradedModel;
        this.speechTexts = presenter.getSpeechTexts(upgradedModel['speechTexts']);
        this.globalView = $(view);
        this.createView(view, upgradedModel);
        presenter.view = view;
        if (!isPreview) {
        	this.loadFiles(this.audio, upgradedModel);
            MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }
        this.visible = !!(upgradedModel['Is Visible'] == 'True');
        this.defaultVisibility = this.visible;
    };

    presenter.destroy = function AddonMultiAudio_destroy() {
        presenter.audio.pause();
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'play': presenter.play,
            'stop': presenter.stop,
            'next': presenter.next,
            'previous': presenter.previous,
            'jumpTo': presenter.jumpToCommand,
            'jumpToID': presenter.jumpToIDCommand,
            'pause': presenter.pause
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        $(presenter.globalView).css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.play = deferredSyncQueue.decorate(function() {
        if (!this.audio.playing) {
            this.audio.play();
        }
        if (presenter.globalModel["Interface"] == "Draggable items") {
            presenter.setCurrentDraggableItemToPlay();
        }
    });

    presenter.setCurrentDraggableItemToPlay = function() {
        var itemID = presenter.files[presenter.currentAudio].ID;
        $(presenter.view).find(".multiaudio-item-wrapper").each(function(){
            var $this = $(this);
            if ($this.attr('data-audio-id') == itemID) {
                $this.addClass('playing');
            } else {
                $this.removeClass('playing');
            }
        });
    };

    presenter.stop = deferredSyncQueue.decorate(function() {
        if (!presenter.audio.paused) {
            presenter.audio.pause();
            presenter.playingEventSent = false;
            presenter.sendOnPausedEvent();
        }

        presenter.audio.currentTime = 0;
        if (presenter.globalModel["Interface"] == "Draggable items") {
            presenter.stopDraggableItems();
        }
    });

    presenter.stopDraggableItems = function() {
        $(presenter.view).find('.multiaudio-item-wrapper.playing').removeClass('playing');
    };

    presenter.pause = function() {
        if (!presenter.audio.paused) {
            presenter.audio.pause();
            presenter.playingEventSent = false;
            presenter.sendOnPausedEvent();
        }
        presenter.stopDraggableItems();
    };

    presenter.show = function() {
        this.setVisibility(true);
        this.visible = true;
        if(audioStarted(this.audio)) {
            this.audio.play();
        }
    };

    function increaseVolume() {
        var volume = presenter.audio.volume;
        volume += 0.1;
        if (volume &gt; 1.0) volume = 1.0;
        presenter.audio.volume = volume;
    };

    function decreaseVolume() {
        var volume = presenter.audio.volume;
        volume -= 0.1;
        if (volume &lt; 0.0) volume = 0.0;
        presenter.audio.volume = volume;
    };

    function forward() {
        presenter.audio.currentTime += 5;
    }

    function backward() {
            presenter.audio.currentTime -= 5;
        }

    function playPause() {
        if (presenter.audio.paused) {
            presenter.play();
        } else {
            presenter.pause();
        }
    }

    function playStop() {
            if (presenter.audio.paused) {
                presenter.play();
            } else {
                presenter.stop();
            }
        }

    presenter.hide = function() {
        this.setVisibility(false);
        this.visible = false;
        if(audioStarted(this.audio)){
            this.audio.pause();
        }
    };

    function audioStarted(audio) {
        return audio.currentTime &gt; 0;
    }
    
    presenter.reset = function() {
        this.visible = this.defaultVisibility;
        if (this.visible) {
            this.show();
        } else {
            this.hide();
        }
        this.stop();
        this.currentAudio = 0;
        presenter.loadFiles(this.audio, this.globalModel);

        if (presenter.globalModel["Interface"] == "Draggable items") {
            removeDraggableItems();
            for (var i = 0; i &lt; presenter.globalModel.Files.length; i++) {
                createDraggableItem(presenter.globalModel.Files[i].ID);
            }
        }
    };

    presenter.jumpTo = function(audioNumber) {
        var newAudio = parseInt(audioNumber, 10) - 1;
        if (0 &lt;= newAudio &amp;&amp; newAudio &lt; this.files.length) {
            presenter.sendOnPausedEvent();
            this.currentAudio = newAudio;
            presenter.isLoaded = false;
            presenter.loadFiles(this.audio, this.globalModel);
        }
    };

    presenter.jumpToCommand = function(params) {
        presenter.jumpTo(params[0]);
        presenter.playingEventSent = false;
    };

    presenter.previous = function() {
        if (this.currentAudio &gt; 0) {
            presenter.sendOnPausedEvent();
            this.currentAudio--;
            this.initialize(this.globalView[0], this.globalModel);
        }
    };

    presenter.next = function() {
        if (this.currentAudio &lt; this.files.length - 1) {
            presenter.sendOnPausedEvent();
            this.currentAudio++;
            this.initialize(this.globalView[0], this.globalModel);
        }
    };

    presenter.previousDraggableItem = function() {
        var itemIDs = Object.keys(presenter.draggableItems);
        if (itemIDs.length == 0){
            presenter.currentDraggableItemID = '';
            return;
        }
        if (presenter.currentDraggableItemID.length == 0) {
            presenter.currentDraggableItemID = itemIDs[itemIDs.length - 1];
            presenter.jumpToID(presenter.currentDraggableItemID);
            updateWCAGSelectedClass();
        } else {
            var index = itemIDs.indexOf(presenter.currentDraggableItemID);
            if (index &gt; 0) {
                presenter.currentDraggableItemID = itemIDs[index-1];
                presenter.jumpToID(presenter.currentDraggableItemID);
                updateWCAGSelectedClass();
            }
        }
        }

    presenter.nextDraggableItem = function() {
        var itemIDs = Object.keys(presenter.draggableItems);
        if (itemIDs.length == 0){
            presenter.currentDraggableItemID = '';
            return;
        }
        if (presenter.currentDraggableItemID.length == 0) {
            presenter.currentDraggableItemID = itemIDs[0];
            presenter.jumpToID(presenter.currentDraggableItemID);
            updateWCAGSelectedClass();
        } else {
            var index = itemIDs.indexOf(presenter.currentDraggableItemID);
            if (index &lt; itemIDs.length -1 &amp;&amp; index != -1) {
                presenter.currentDraggableItemID = itemIDs[index+1];
                presenter.jumpToID(presenter.currentDraggableItemID);
                updateWCAGSelectedClass();
            }
        }
    }

    function updateWCAGSelectedClass() {
        if (presenter.globalModel["Interface"] != "Draggable items") return;

        clearWCAGSelectedClass();
        if (presenter.currentDraggableItemID.length &gt; 0) {
            presenter.draggableItems[presenter.currentDraggableItemID].addClass('keyboard_navigation_active_element');
        }
    }

    function clearWCAGSelectedClass() {
        var activeClassName = 'keyboard_navigation_active_element';
        presenter.globalView.find('.'+activeClassName).removeClass(activeClassName);
    }

    presenter.jumpToID = function(id) {
        for (var i = 0; i &lt; this.files.length; i++) {
            if (id === this.files[i].ID) {
                this.jumpTo(i + 1);  // Audio numbers are counted from 1 to n
                break;
            }
        }
    };

    presenter.jumpToIDCommand = function(params) {
        presenter.jumpToID(params[0]);
    };

    presenter.getState = function() {
        var state = {
            'visible' : "" + this.visible,
            'currentAudio' : "" + this.currentAudio,
            'currentTime'   : "" + this.audio.currentTime
        };
        var draggableKeys = Object.keys(presenter.draggableItems);
        if (draggableKeys.length != 0) {
            state['draggableItems'] = Object.keys(presenter.draggableItems).join(',')
        }
        return this.convertStateToString(state);
    };

    presenter.convertStateToString = function(state) {
        var stateString = "";
        $.each(state, function(key, value){
            stateString += "[" + key + ":" + value + "]";
        });
        return stateString;
    };

    presenter.convertStringToState = function(stateString) {
        var state = {};
        var pattern = /\w+:[\w|\,]+/g;
        var stateElements = stateString.match(pattern);
        for (var i = 0; i &lt; stateElements.length; i++) {
            var keyAndValue = stateElements[i].split(":");
            var key = keyAndValue[0];
            state[key] = keyAndValue[1];
        }
        return state;
    };

    presenter.setState = function(stateString) {
        var state = this.convertStringToState(stateString);
        var visible = !!(state["visible"] == "true");
        var currentAudio = parseInt(state["currentAudio"]);
        var currentTime = parseInt(state["currentTime"]);

        if (visible) {
            this.show();
        } else {
            this.hide();
        }

        this.currentAudio = currentAudio;
        presenter.loadFiles(this.audio, this.globalModel);

        if (presenter.globalModel["Interface"] == "Draggable items") {
            var keys = Object.keys(presenter.draggableItems);
            if ('draggableItems' in state) {
                var loadedDraggableItems = state['draggableItems'].split(',');
                for (var i = 0; i &lt; keys.length; i++) {
                    if (loadedDraggableItems.indexOf(keys[i]) == -1) {
                        removeDraggableItem(keys[i]);
                    }
                }
            } else {
                for (var i = 0; i &lt; keys.length; i++) {
                    removeDraggableItem(keys[i]);
                }
            }
        }
    };

    presenter.validateFiles = function(files) {
        return !(!files["Ogg"] &amp;&amp; !files["Mp3"]);
    };

    presenter.setWCAGStatus = function (isOn) {
            presenter.isWCAGOn = isOn;
            if (!isOn) {
                clearWCAGSelectedClass();
                presenter.currentDraggableItemID = '';
            }
        };

    presenter.keyboardController = function (keycode, isShift, event) {
        event.preventDefault();
        if (presenter.globalModel["Interface"] == "Draggable items") {
            presenter.draggableKeyboardController(keycode, isShift, event);
        } else {
            presenter.audioKeyboardController(keycode, isShift, event);
        }
    };

    presenter.draggableKeyboardController = function (keycode, isShift, event) {
        switch (keycode) {
            case 9: // TAB
                if (isShift) {
                    presenter.previousDraggableItem();
                } else {
                    presenter.nextDraggableItem();
                }
                readCurrentDraggableFileText();
                break;
            case 13: //ENTER
                if (isShift) {
                    presenter.stop();
                    clearWCAGSelectedClass();
                    presenter.currentDraggableItemID = '';
                } else {
                    if (presenter.currentDraggableItemID.length &gt; 0) {
                        playStop();
                        updateWCAGSelectedClass();
                    }
                }
                break;
            case 32: //SPACE
                var itemID = presenter.files[presenter.currentAudio].ID;
                presenter.handleGrabAreaClick(itemID);
                break;
            case 38: // UP
                presenter.previousDraggableItem();
                readCurrentDraggableFileText();
                updateWCAGSelectedClass();
                break;
            case 40: // DOWN
                presenter.nextDraggableItem();
                readCurrentDraggableFileText();
                updateWCAGSelectedClass();
                break;
            case 37: // LEFT
                presenter.previousDraggableItem();
                readCurrentDraggableFileText();
                updateWCAGSelectedClass();
                break;
            case 39: // RIGHT
                presenter.nextDraggableItem();
                readCurrentDraggableFileText();
                updateWCAGSelectedClass();
                break;
            case 27: // ESC
                presenter.stop();
                clearWCAGSelectedClass();
                presenter.currentDraggableItemID = '';
                break;
        }
    }

    presenter.audioKeyboardController = function (keycode, isShift, event) {
        switch (keycode) {
            case 9: // TAB
                if (isShift) {
                    presenter.previous();
                } else {
                    presenter.next();
                }
                readCurrentAudioFileText();
                break;
            case 13: //ENTER
                if (!isShift) {
                    presenter.pause();
                    readCurrentAudioFileText();
                }
                break;
            case 32: // SPACE
                playPause();
                break;
            case 38: // UP
                increaseVolume();
                break;
            case 40: // DOWN
                decreaseVolume();
                break;
            case 37: // LEFT
                backward();
                break;
            case 39: // RIGHT
                forward();
                break;
            case 27: // ESC
                presenter.stop();
                break;
        }
    }

    function readSelected() {
        var textVoiceArray = [];
        textVoiceArray.push(getTextVoiceObject(presenter.speechTexts.selected, ""));
        speak(textVoiceArray);
    }

    function readDeselected() {
        var textVoiceArray = [];
        textVoiceArray.push(getTextVoiceObject(presenter.speechTexts.deselected, ""));
        speak(textVoiceArray);
    }

    function readCurrentDraggableFileText() {
        var textVoiceArray = [];
        if (Object.keys(presenter.draggableItems).length == 0) {
            textVoiceArray.push(getTextVoiceObject(presenter.speechTexts.empty, ""));
        } else {
            var item = presenter.files[presenter.currentAudio];
            textVoiceArray.push(getTextVoiceObject(item.Text, presenter.globalModel['langAttribute']));
            if (presenter.selectedItemID == item.ID) {
                textVoiceArray.push(getTextVoiceObject(presenter.speechTexts.selected, ""));
            }
        }
        speak(textVoiceArray);
    }

    function readCurrentAudioFileText() {
        var item = presenter.files[presenter.currentAudio];
        var textVoiceArray = [getTextVoiceObject(item.Text, presenter.globalModel['langAttribute'])];
        speak(textVoiceArray);
    }

    presenter.getTextToSpeechOrNull = function () {
        if (presenter.playerController) {
            return presenter.playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull();
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Multiple_Audio_Controls_Binder" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Connections" nameLabel="Multiple_Audio_Controls_Binder_property_connections" type="text"/>
	</model>
<css/><view/><preview/><presenter>function AddonMultiple_Audio_Controls_Binder_create() {
    var presenter = function () {};
    presenter.configuration = {};

    presenter.ERROR_CODES = {
        'CONNECTIONS_01': "Connections cannot be empty!",
        'CONNECTIONS_02': "Missing separator character!",
        'CONNECTIONS_03': "Missing Audio addon ID!",
        'CONNECTIONS_04': "Missing Double State Button addon ID!",
        'CONNECTIONS_05': "Empty lines are not allowed!",
        'CONNECTIONS_06': "Repeated Audio addon ID!",
        'CONNECTIONS_07': "Repeated Double State Button addon ID!",
        'CONNECTIONS_08': "Missing item number!",
        'CONNECTIONS_09': "Item must be positive number!"
    };

    presenter.STATES = {
        AUDIO: {
            PLAYING: 1,
            STOPPED: 2
        },

        DOUBLE_STATE_BUTTON: {
            SELECTED: 1,
            DESELECTED: 2
        }
    };

    function showErrorMessage(errorCode) {
        presenter.$view.html(presenter.ERROR_CODES[errorCode]);
    }

    presenter.validateModel = function AddonMultiple_Audio_Controls_Binder_validateModel (model) {
        var validatedConnections = presenter.validateConnections(model.Connections);

        if (!validatedConnections.isValid) {
            return {
                isValid: false,
                errorCode: validatedConnections.errorCode
            }
        }

        return {
            isValid: true,
            connections:  new presenter.Connections(validatedConnections.connections)
        }
    };

    function presenterLogic (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.configuration = presenter.validateModel(model);

        if (!presenter.configuration.isValid) {
            showErrorMessage(presenter.configuration.errorCode);

            delete presenter.getState;
            delete presenter.setState;

            return;
        }

        if (presenter.eventBus) {
            presenter.eventBus.addEventListener('PageLoaded', presenter);
        }

        if (isPreview) return;

        presenter.$view.css('visible', 'hidden');
    }

    presenter.isAudioIDPresent = function (connections, audioID) {
        var i;

        for (i = 0; i &lt; connections.length; i++) {
            if (connections[i].Audio === audioID) return true;
        }

        return false;
    };

    presenter.isDoubleStateButtonIDPresent = function (connections, doubleStateButtonID) {
        var i;

        for (i = 0; i &lt; connections.length; i++) {
            if (connections[i].DoubleStateButton === doubleStateButtonID) return true;
        }

        return false;
    };

    presenter.validateConnections = function AddonMultiple_Audio_Controls_Binder_validateConnections(connections) {
        var parsedConnections = [], isValid = true, errorCode;

        if (ModelValidationUtils.isStringEmpty(connections)) return { isValid: false, errorCode: 'CONNECTIONS_01'};

        jQuery.each(connections.split('\n'), function (elementIndex, element) {
            var indexOfSeparator, modules, audioID, doubleStateButtonID, itemNumber;

            if (ModelValidationUtils.isStringEmpty(element)) {
                isValid = false;
                errorCode = 'CONNECTIONS_05';
                return false;
            }

            indexOfSeparator = element.indexOf('|');
            if (indexOfSeparator === -1) {
                isValid = false;
                errorCode = 'CONNECTIONS_02';
                return false;
            }

            modules = element.split('|');
            audioID = modules[0];
            doubleStateButtonID = modules[1];
            itemNumber = modules[2];

            if (ModelValidationUtils.isStringEmpty(audioID)) {
                isValid = false;
                errorCode = 'CONNECTIONS_03';
                return false;
            }

            if (ModelValidationUtils.isStringEmpty(doubleStateButtonID)) {
                isValid = false;
                errorCode = 'CONNECTIONS_04';
                return false;
            }

            if (itemNumber === undefined &amp;&amp; presenter.isAudioIDPresent(parsedConnections, audioID)) {
                isValid = false;
                errorCode = 'CONNECTIONS_06';
                return false;
            }

            if (presenter.isDoubleStateButtonIDPresent(parsedConnections, doubleStateButtonID)) {
                isValid = false;
                errorCode = 'CONNECTIONS_07';
                return false;
            }

            if (itemNumber !== undefined &amp;&amp; ModelValidationUtils.isStringEmpty(itemNumber)) {
                isValid = false;
                errorCode = 'CONNECTIONS_08';
                return false;
            }

            if (itemNumber !== undefined &amp;&amp; !ModelValidationUtils.validatePositiveInteger(itemNumber).isValid) {
                isValid = false;
                errorCode = 'CONNECTIONS_09';
                return false;
            }

            parsedConnections.push({ Audio: audioID, DoubleStateButton: doubleStateButtonID, Item: itemNumber });
        });

        return {
            isValid: isValid,
            connections: parsedConnections,
            errorCode: errorCode
        };
    };

    presenter.Connection = function (audioID, doubleStateButtonID, ID, item) {
        var self = this;

        this.DoubleStateButton = {
            ID: doubleStateButtonID,
            getModule: function () { return presenter.getModule(self.DoubleStateButton.ID); },
            state: presenter.STATES.DOUBLE_STATE_BUTTON.DESELECTED
        };

        this.Audio = {
            ID: audioID,
            getModule: function () { return new presenter.AudioAdapter(presenter.getModule(self.Audio.ID)); },
            state: presenter.STATES.AUDIO.STOPPED
        };

         this.Item = {
            Digit: item
        };

        this.ID = ID;
    };

    presenter.Connections = function (connections) {
        this.connections = [];

        for (var i = 0; i &lt; connections.length; i++) {
            var audioID = connections[i].Audio;
            var doubleStateButtonID = connections[i].DoubleStateButton;
            var item = connections[i].Item;

            this.connections.push(new presenter.Connection(audioID, doubleStateButtonID, i, item));
        }

        this.getConnection = function (connectionID) {
            return jQuery.grep(this.connections, function (element) {
                return element.ID == connectionID;
            })[0];
        };

        this.getConnectionWithAudio = function (audioID) {
            for (var i = 0; i &lt; this.connections.length; i++) {
                if (this.connections[i].Audio.ID == audioID) return this.connections[i];
            }

            return undefined;
        };

        this.getConnectionWithAudioAndItem = function (audioID, item) {
            for (var i = 0; i &lt; this.connections.length; i++) {
                if (this.connections[i].Audio.ID == audioID &amp;&amp; this.connections[i].Item.Digit == item) return this.connections[i];
            }

            return undefined;
        };

        this.getConnectionWithDSB = function (doubleStateButtonID) {
            for (var i = 0; i &lt; this.connections.length; i++) {
                if (this.connections[i].DoubleStateButton.ID == doubleStateButtonID) return this.connections[i];
            }

            return undefined;
        };

        this.getConnectionsOtherThan = function (connectionID) {
            return jQuery.grep(this.connections, function (connection) {
                return connection.ID != connectionID;
            });
        };

        this.getAllConnections = function () {
            return this.connections;
        };
    };

    presenter.getModule = function (moduleID) {
        return presenter.playerController.getModule(moduleID);
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('ValueChanged', this);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        }

        var matchedModule = presenter.matchEventToModules(eventData);

        if (!matchedModule.isMatch) return;

        switch (matchedModule.action) {
            case presenter.EVENT_ACTIONS.AUDIO_END:
                presenter.audioEndHandler(matchedModule.moduleID, eventData.item);
                break;
            case presenter.EVENT_ACTIONS.DOUBLE_STATE_BUTTON_SELECT:
                presenter.doubleStateButtonSelectionHandler(matchedModule.moduleID);
                break;
            case presenter.EVENT_ACTIONS.DOUBLE_STATE_BUTTON_DESELECT:
                presenter.doubleStateButtonDeselectionHandler(matchedModule.moduleID);
                break;
        }
    };

    presenter.audioEndHandler = function (audioID, item) {
        var connection = presenter.getAudioAdapterConnection(audioID, item);

        connection.Audio.state = presenter.STATES.AUDIO.STOPPED;
        connection.DoubleStateButton.getModule().deselect();
        connection.DoubleStateButton.state = presenter.STATES.DOUBLE_STATE_BUTTON.DESELECTED;
    };

    presenter.getAudioAdapterConnection = function (audioID, item) {
        if (presenter.getModule(audioID).type === 'multiaudio') {
            return presenter.configuration.connections.getConnectionWithAudioAndItem(audioID, item);
        }

        return presenter.configuration.connections.getConnectionWithAudio(audioID);
    };

    presenter.doubleStateButtonSelectionHandler = function (moduleID) {
        var connection = presenter.configuration.connections.getConnectionWithDSB(moduleID),
            otherConnections = presenter.configuration.connections.getConnectionsOtherThan(connection.ID),
            audio, doubleStateButton;

        jQuery.each(otherConnections, function (index, connection) {
            audio = connection.Audio;
            doubleStateButton = connection.DoubleStateButton;
            if (presenter.STATES.AUDIO.PLAYING == audio.state) {
                audio.getModule().stop();
                audio.state = presenter.STATES.AUDIO.STOPPED;
                doubleStateButton.getModule().deselect();
                doubleStateButton.state = presenter.STATES.DOUBLE_STATE_BUTTON.DESELECTED;
            }
        });

        connection.Audio.getModule().play(connection.Item.Digit);
        connection.Audio.state = presenter.STATES.AUDIO.PLAYING;
        connection.DoubleStateButton.state = presenter.STATES.DOUBLE_STATE_BUTTON.SELECTED;
    };

    presenter.doubleStateButtonDeselectionHandler = function (moduleID) {
        var connection = presenter.configuration.connections.getConnectionWithDSB(moduleID);

        connection.Audio.getModule().stop();
        connection.Audio.state = presenter.STATES.AUDIO.STOPPED;
        connection.DoubleStateButton.state = presenter.STATES.DOUBLE_STATE_BUTTON.DESELECTED;
    };

    presenter.matchEventToModules = function (eventData) {
        var moduleID = eventData.source,
            connection = presenter.configuration.connections.getConnectionWithAudio(moduleID),
            eventActions = presenter.EVENT_ACTIONS;

        if (connection) {
            if (eventData.item !== 'end' &amp;&amp; eventData.value !== 'end') return { isMatch: false };

            return {
                isMatch: true,
                moduleID: moduleID,
                moduleType: presenter.MODULE_TYPE.AUDIO,
                action: eventActions.AUDIO_END,
                connectionID: connection.ID
            };
        }

        // No Audio module with given ID
        connection = presenter.configuration.connections.getConnectionWithDSB(moduleID);
        if (!connection) return { isMatch: false };

        return {
            isMatch: true,
            moduleType: presenter.MODULE_TYPE.DOUBLE_STATE_BUTTON,
            moduleID: moduleID,
            action: eventData.value == '1' ? eventActions.DOUBLE_STATE_BUTTON_SELECT : eventActions.DOUBLE_STATE_BUTTON_DESELECT,
            connectionID: connection.ID
        };
    };

    presenter.EVENT_ACTIONS = {
        AUDIO_END: 0,
        DOUBLE_STATE_BUTTON_SELECT: 1,
        DOUBLE_STATE_BUTTON_DESELECT: 2
    };

    presenter.MODULE_TYPE = {
        AUDIO: 0,
        DOUBLE_STATE_BUTTON: 1
    };

    presenter.run = function (view, model) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();
        presenterLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenterLogic(view, model, true);
    };

    presenter.getState = function () {
        var state = jQuery.map(presenter.configuration.connections.getAllConnections(), function (connection) {
            var isSelected = presenter.STATES.DOUBLE_STATE_BUTTON.SELECTED === connection.DoubleStateButton.state;

            return { ID: connection.ID, isSelected: isSelected };
        });

        return JSON.stringify(state);
    };

    presenter.setState = function (stringifiedState) {
        var state = JSON.parse(stringifiedState);

        state = jQuery.grep(state, function (element) {
            return element.isSelected;
        });

        presenter.pageLoadedHandlerLoad(state);
    };

    presenter.pageLoadedHandlerLoad = function (state) {
        var connection;
        presenter.pageLoaded.then(function() {
            jQuery.each(state, function (index, value) {
                connection = presenter.configuration.connections.getConnection(value.ID);
                connection.DoubleStateButton.getModule().deselect();
            });
        });
    };

    presenter.AudioAdapter = function AddonMultiple_Audio_Controls_Binder_AudioAdapter (audioPresenter) {
        this.audioPresenter = audioPresenter;
    };

    presenter.AudioAdapter.prototype.play = function (item) {
        if (this.audioPresenter === undefined || this.audioPresenter === null) return;

        if (this.audioPresenter.type === 'multiaudio') {
            this.audioPresenter.jumpTo(item);
        }
        this.audioPresenter.play();
    };

     presenter.AudioAdapter.prototype.stop = function () {
         if (this.audioPresenter === undefined || this.audioPresenter === null) return;

         this.audioPresenter.stop();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Navigation_Bar" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property displayName="Show Next and Prev arrows" name="ShowNextPrevArrows" nameLabel="Navigation_Bar_property_show_next_prev_arrows" type="boolean"/>
		<property displayName="Hide Home and Last arrows" name="HideHomeLastArrows" nameLabel="Navigation_Bar_property_show_home_last_arrows" type="boolean"/>
		<property name="Numericals" nameLabel="Navigation_Bar_property_numericals" type="{Western Arabic,Eastern Arabic,Perso-Arabic}"/>
        <property displayName="Styles" name="Styles" nameLabel="Navigation_Bar_property_styles" type="list">
            <property name="Pages" nameLabel="Navigation_Bar_property_pages" type="string"/>
            <property name="Class" nameLabel="Navigation_Bar_property_class" type="string"/>
            <property name="Style" nameLabel="Navigation_Bar_property_style" type="string"/>
        </property>
        <property displayName="Real-time navigationbar-page-ok" name="AddClassNBPageOK" nameLabel="Navigation_Bar_property_add_class_nb_page_ok" type="boolean"/>
        <property displayName="First page as cover" name="firstPageAsCover" nameLabel="Navigation_Bar_property_first_page_as_cover" type="boolean"/>
        <property displayName="Last page separated" name="lastPageSeparated" nameLabel="Navigation_Bar_property_last_page_separated" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Navigation_Bar_property_speech_texts" type="staticlist">
            <property name="GoToPageNumber" nameLabel="Navigation_Bar_property_go_to_page_number" type="staticrow">
                <property name="GoToPageNumber" nameLabel="Navigation_Bar_property_go_to_page_number" type="string"/>
            </property>
            <property name="NextPage" nameLabel="Navigation_Bar_property_go_to_next_page" type="staticrow">
                <property name="NextPage" nameLabel="Navigation_Bar_property_go_to_next_page" type="string"/>
            </property>
            <property name="PrevPage" nameLabel="Navigation_Bar_property_go_to_prev_page" type="staticrow">
                <property name="PrevPage" nameLabel="Navigation_Bar_property_go_to_prev_page" type="string"/>
            </property>
            <property name="Titled" nameLabel="Navigation_Bar_property_titled" type="staticrow">
                <property name="Titled" nameLabel="Navigation_Bar_property_titled" type="string"/>
            </property>
            <property name="DottedRight" nameLabel="Navigation_Bar_property_show_more" type="staticrow">
                <property name="DottedRight" nameLabel="Navigation_Bar_property_show_more" type="string"/>
            </property>
            <property name="DottedLeft" nameLabel="Navigation_Bar_property_show_prev" type="staticrow">
                <property name="DottedLeft" nameLabel="Navigation_Bar_property_show_prev" type="string"/>
            </property>
            <property name="DisabledPage" nameLabel="Navigation_Bar_property_disabled_page" type="staticrow">
                <property name="DisabledPage" nameLabel="Navigation_Bar_property_disabled_page" type="string"/>
            </property>
        </property>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Navigation_Bar_property_lang_attribute" type="string"/>
        <property name="playTitle" nameLabel="Navigation_Bar_property_play_title" type="boolean"/>
        <property displayName="Block not visited pages" name="blockNotVisited" nameLabel="Navigation_Bar_property_block_not_visited" type="boolean"/>
	</model>
<css>.navigationbar-wrapper {
    direction: inherit;
}

.navigationbar-wrapper * {
    direction: inherit;
}

.navigationbar-wrapper a {
    text-decoration: none !important;
}

.navigationbar-wrapper a span {
    display: block;
    float: left;
    text-align: center;
    white-space: nowrap;
    text-decoration: none;
    height: auto;
    font: 1.5em "Lucida Grande", Geneva, Verdana, Arial, Helvetica, sans-serif;
    width: 40px;
}

.navigationbar-element,
.navigationbar-element-first,
.navigationbar-element-next,
.navigationbar-element-previous,
.navigationbar-element-last {
    color: #7F7F7F;
    background-color: #F3F3F3;
}

.navigationbar-element-current {
    background-color: #2091AB;
    color: white;
}

.navigationbar-element-mouse-hover,
.navigationbar-element-first-mouse-hover,
.navigationbar-element-next-mouse-hover,
.navigationbar-element-previous-mouse-hover,
.navigationbar-element-last-mouse-hover {
    background-color: #7F7F7F;
    color: #F3F3F3;
}

.navigationbar-element-current-mouse-hover {
    background-color: #17697B;
}

.navigationbar-element-first-inactive,
.navigationbar-element-next-inactive,
.navigationbar-element-previous-inactive,
.navigationbar-element-last-inactive {
    background-color: #80BBC8;
    color: #C0C0C0;
}

.navigationbar-element-mouse-click,
.navigationbar-element-current-mouse-click,
.navigationbar-element-first-mouse-click,
.navigationbar-element-next-mouse-click,
.navigationbar-element-previous-mouse-click,
.navigationbar-element-last-mouse-click {
    background-color: #0A7058;
    color: white;
}

.navigationbar-wrapper .disabled {
    color: #C0C0C0;
}

.navigationbar-wrapper .bookmark {
    color: red;
}
</css><view>&lt;div class="navigationbar-wrapper"&gt;
    &lt;a title="First page" href="#"&gt;
        &lt;span class="navigationbar-element-first"&gt;&amp;lt;&amp;lt;&lt;/span&gt;
    &lt;/a&gt;
    &lt;a title="Last page" href="#"&gt;
        &lt;span class="navigationbar-element-last"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
    &lt;/a&gt;
&lt;/div&gt;
</view><preview>&lt;div class="navigationbar-wrapper"&gt;
    &lt;a title="First page" href="#"&gt;
        &lt;span class="navigationbar-element-first"&gt;&amp;lt;&amp;lt;&lt;/span&gt;
    &lt;/a&gt;
    &lt;a title="Last page" href="#"&gt;
        &lt;span class="navigationbar-element-last"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
    &lt;/a&gt;
&lt;/div&gt;
</preview><presenter>function AddonNavigation_Bar_create() {
    var presenter = function () { };
    presenter.eventBus = null;
    presenter.pagesOk = [];
    presenter.allPagesDisplayed = false;
    presenter.pageTitles = [];
    presenter.visitedPages = [];
    presenter.originalIndex = 0;

    var isWCAGOn = false;

    presenter.state = {
        bookmarks: []
    };

    presenter.__internalElements = {
        goToPage: goToPage
    };

    var NAVIGATION_PAGE = {
        FIRST: 0,
        LAST: 1,
        PREVIOUS: 2,
        NEXT: 3,
        OTHER: 4
    };

    presenter.ERROR_CODES = {
        'E_01': "Pages and Style or Class attribute in 'Styles' must be filled",
        'E_04': "Pages attribute in Styles may contain only previous, next, first, last and positive integer page numbers",
        'P_01': "Cannot load module - HTML element doesn't exists"
    };

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    var DOTTED_SIDE = {
        LEFT: { CSSClass: "dotted-element-left" },
        RIGHT: { CSSClass: "dotted-element-right" }
    };

    var movedFromIndex,
        maxElementCount;

    function getLanguage(model) {
    	if (model['Numericals'] == 'Eastern Arabic') {
    		return Internationalization.EASTERN_ARABIC;
    	}
    	if (model['Numericals'] == 'Perso-Arabic') {
    		return Internationalization.PERSO_ARABIC;
    	}
    	return Internationalization.WESTERN_ARABIC;
    }

    function getTextVoiceObject (text, lang) { return {text: text, lang: lang}; }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.keyboardController = function(keycode, isShift, event) {
        event.preventDefault();

        var elements = presenter.$view.find("span").not("[class*='inactive']");

        function getCurrentPosition() {
            var pos;
            $.each(elements, function(i, el) {
                if ($(el).is("[class*='mouse-hover']")) {
                    pos = i;
                    return false;
                }
            });

            if (typeof pos === "undefined") {
                $.each(elements, function(i, el) {
                    if ($(el).is("[class*='current']")) {
                        pos = i;
                        return false;
                    }
                });
            }
            // dotted
            if (typeof pos === "undefined") {
                $.each(elements, function(i, el) {
                    if ($(el).hasClass("navigationbar-dotted-element")) {
                        pos = i;
                        return false;
                    }
                });
            }

            return pos;
        }

        function select(element) {
            if (!element) return;

            deselect();

            $(element).removeClass('navigationbar-element');
            $(element).addClass('navigationbar-element-mouse-hover');

            var pageNumber = $(element).attr('data-page-number');
        }

        function skipToPage() {
            $(elements[getCurrentPosition()]).trigger('click');
        }

        function back() {
            select(elements[getCurrentPosition() - 1]);
            presenter.playButton(elements[getCurrentPosition()]);
        }

        function forward() {
            select(elements[getCurrentPosition() + 1]);
            presenter.playButton(elements[getCurrentPosition()]);
        }

        function deselect() {
            var activeElement = presenter.$view.find('span.navigationbar-element-mouse-hover');
            activeElement.removeClass('navigationbar-element-mouse-hover');
            activeElement.addClass('navigationbar-element');
        }

        switch(keycode) {
            case 13: // enter
                if(isShift) { // isShift is true when ctrl or shift is pressed
                    deselect();
                } else {
                    presenter.playButton(elements[getCurrentPosition()]);
                }
                break;
            case 32: // space
                skipToPage();
                break;
            case 37: // left arrow
                back();
                break;
            case 39: // right arrow
                forward();
                break;
            case 27: // escape
                deselect();
                break;
            case 9: // tab
                if(isShift) {
                    back();
                } else {
                    forward();
                };
                break;
        }
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.presentation = controller.getPresentation();
        presenter.commander = controller.getCommands();
        presenter.pageCount = controller.getPresentation().getPageCount();
        presenter.currentIndex = controller.getCurrentPageIndex();
        presenter.scoreService = controller.getScore();
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('ValueChanged', this);
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('closePage', this);
        presenter.eventBus.addEventListener('visitedPagesUpdate', this);

        reloadVisitedPages();
        presenter.originalIndex = presenter.playerController.getCurrentPageIndex();
    };

    function reloadVisitedPages() {
        presenter.pageTitles = [];
        presenter.visitedPages = [];
        for(var i = 0; i &lt; presenter.pageCount; i++) {
            var page = presenter.presentation.getPage(i);
            presenter.pageTitles.push(page.getName());
            presenter.visitedPages.push(page.isVisited(true));
        }
    }

    presenter.playButton = function(element){
        var $element = $(element);
        if($element.hasClass('navigationbar-element-previous')) {
            presenter.playPage(presenter.currentIndex - 1 , presenter.configuration.speechTexts.prevPage);
        } else if ($element.hasClass('navigationbar-element-next')){
            presenter.playPage(presenter.currentIndex + 1 , presenter.configuration.speechTexts.nextPage);
        } else if ($element.hasClass('dotted-element-left')) {
            speak([getTextVoiceObject(presenter.configuration.speechTexts.dottedLeft)]);
        } else if ($element.hasClass('dotted-element-right')) {
            speak([getTextVoiceObject(presenter.configuration.speechTexts.dottedRight)]);
        } else {
            var pageNumber = $element.attr('data-page-number');
            if (pageNumber !== null &amp;&amp; pageNumber !== undefined &amp;&amp; !isNaN(pageNumber)){
                if ($element.hasClass('disabled')) {
                   presenter.playPage(pageNumber - 1, presenter.configuration.speechTexts.disabledPage + ' ' + pageNumber);
                } else {
                    presenter.playPage(pageNumber - 1, presenter.configuration.speechTexts.goToPageNumber + ' ' + pageNumber);
                }
            }
        }
    };

    presenter.playPage = function (index, text) {
        if (index &gt;= 0 &amp;&amp; index &lt; presenter.pageTitles.length) {
            var textVoiceArray = [];
            if(presenter.configuration.playTitle) {
                textVoiceArray.push(getTextVoiceObject(text + ' ' + presenter.configuration.speechTexts.titled));
                textVoiceArray.push(getTextVoiceObject(presenter.pageTitles[index], presenter.configuration.langTag));
            } else {
                textVoiceArray.push(getTextVoiceObject(text));
            }
            speak(textVoiceArray);
        } else {
            speak(getTextVoiceObject(text));
        }
    };

    function goToPage(whereTo, index) {
        var currentIndex = 0;
        if (movedFromIndex) {
            currentIndex = movedFromIndex;
        } else {
            currentIndex = presenter.playerController.getCurrentPageIndex();
        }
        var goToIndex = 0;
        switch (whereTo) {
            case NAVIGATION_PAGE.FIRST:
                if (currentIndex !== 0) {
                    goToIndex = 0;
                }
                break;
            case NAVIGATION_PAGE.LAST:
                if (currentIndex !== (presenter.pageCount - 1)) {
                    goToIndex = presenter.pageCount - 1;
                }
                break;
            case NAVIGATION_PAGE.NEXT:
                if (currentIndex !== (presenter.pageCount - 1)) {
                    goToIndex = currentIndex + 1;
                }
                break;
            case NAVIGATION_PAGE.PREVIOUS:
                if (currentIndex !== 0) {
                    goToIndex = currentIndex - 1;
                }
                break;
            case NAVIGATION_PAGE.OTHER:
                if ((index &gt;= 0) &amp;&amp; (index &lt;= (presenter.pageCount - 1))) {
                    goToIndex = index;
                }
                break;
        }

         if (presenter.visitedPages[goToIndex] || !presenter.configuration.blockNotVisited) {
             presenter.commander.gotoPageIndex(goToIndex);
         }
    }

    function handleMouseActions(dotsLeftIndex, dotsRightIndex, elementWidth, elementHeight, preview, horizontalGap) {
        handleArrowClickActions();
        handleIndexClickActions();
        handleDottedClickActions(dotsLeftIndex, dotsRightIndex, elementWidth, elementHeight, preview, horizontalGap);
        handleHoverAndMouseDownActions();
    }

    function handleArrowClickActions() {
        presenter.$view.find('[class="navigationbar-element-first"]').parent().click(function () {
            goToPage(NAVIGATION_PAGE.FIRST);
            return false;
        });

        presenter.$view.find('[class="navigationbar-element-previous"]').parent().click(function () {
            goToPage(NAVIGATION_PAGE.PREVIOUS);
            return false;
        });

        presenter.$view.find('[class="navigationbar-element-next"]').parent().click(function () {
            goToPage(NAVIGATION_PAGE.NEXT);
            return false;
        });

        presenter.$view.find('[class="navigationbar-element-last"]').parent().click(function () {
            goToPage(NAVIGATION_PAGE.LAST);
            return false;
        });
    }

    function handleIndexClickActions() {
        presenter.$view.find('[class*="navigationbar-indexed-element"]').each(function () {
            var pageIndex = parseInt($(this).attr("data-page-number"), 10) - 1;

            $(this).parent().click(function (event) {
                event.stopPropagation();
                event.preventDefault();

                goToPage(NAVIGATION_PAGE.OTHER, pageIndex);
            });
        });

    }

    presenter.checkIfPagesOk = function () {
        presenter.$view.find(".navigationbar-indexed-element").each(function () {
            if($(this).hasClass('navigationbar-page-ok')){
                var pageIndex = parseInt($(this).attr("data-page-number"), 10);
                presenter.pagesOk.push(pageIndex);
            }
        });
    };

    presenter.addClassPageOK = function () {
        for (var i=0; i &lt; presenter.pagesOk.length; i++){
            presenter.$wrapper.find("[data-page-number='" + presenter.pagesOk[i] + "']").addClass('navigationbar-page-ok');
        }
    };

    function handleDottedClickActions(dotsLeftIndex, dotsRightIndex, elementWidth, elementHeight, preview, horizontalGap) {
        presenter.$view.find(".dotted-element-left:first").click(function () {
            if (dotsLeftIndex === undefined || dotsLeftIndex &lt; 0) {
                dotsLeftIndex = 0;
            }

            removeAllElements();
            if (movedFromIndex == undefined) {
                movedFromIndex = presenter.currentIndex;
            }
            presenter.currentIndex = dotsLeftIndex;

            generateElements(elementWidth, elementHeight, true, preview, horizontalGap, false);

            presenter.addClassPageOK();

            presenter.isPageOK();

            if(presenter.configuration.styles) {
                presenter.setPageStyles();
            }

            presenter.refreshBookmarks();
            if(isWCAGOn) {
                presenter.$view.find(".dotted-element-right:first").addClass('navigationbar-element-mouse-hover');
            }
            return false;
        });

        presenter.$view.find(".dotted-element-right:first").click(function () {
            if (dotsRightIndex === undefined || dotsRightIndex &gt; presenter.pageCount - 1) {
                dotsRightIndex = presenter.pageCount - 1;
            }

            removeAllElements();
            if (movedFromIndex == undefined) {
                movedFromIndex = presenter.currentIndex;
            }
            presenter.currentIndex = dotsRightIndex;

            generateElements(elementWidth, elementHeight, true, preview, horizontalGap, false);

            presenter.addClassPageOK();

            presenter.isPageOK();

            if(presenter.configuration.styles) {
                presenter.setPageStyles();
            }

            presenter.refreshBookmarks();
            if(isWCAGOn) {
                presenter.$view.find(".dotted-element-left:first").addClass('navigationbar-element-mouse-hover');
            }
            return false;
        });
    }

    function handleHoverAndMouseDownActions() {
        var isHomeInactive = presenter.currentIndex === 0;
        var isReportInactive = presenter.currentIndex === presenter.pageCount - 1;

        handleSpecificElementHoverAndMouseDown('navigationbar-element-first', isHomeInactive);
        handleSpecificElementHoverAndMouseDown('navigationbar-element-previous', isHomeInactive);
        handleSpecificElementHoverAndMouseDown('navigationbar-element-next', isReportInactive);
        handleSpecificElementHoverAndMouseDown('navigationbar-element-last', isReportInactive);
        handleSpecificElementHoverAndMouseDown('navigationbar-element-current', false);

        handleElementHoverAndMouseDown();
    }

    function handleSpecificElementHoverAndMouseDown(selector, inactive) {
        var removeClassNames = selector + ' ' + selector + '-mouse-hover' +
            ' ' + selector + '-mouse-click' + ' ' + selector + '-inactive';

        var iOS = ( navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false );

        presenter.$wrapper.find("span[class^=" + selector + "]").each(function() {
            var addClassName = inactive ? selector + '-inactive' : selector;

            if (!iOS) {
            	$(this).hover(
            		function() {
                    	$(this).removeClass(removeClassNames);
                    	$(this).addClass(selector + '-mouse-hover');
                	},
                	function() {
                    	$(this).removeClass(removeClassNames);
                    	$(this).addClass(addClassName);
                    	if (presenter.configuration.blockNotVisited) {
                            presenter.setDisabledPagesStyle();
                        }
                	}
            	);
            }

            $(this).mousedown(
                function() {
                    $(this).removeClass(removeClassNames);
                    $(this).addClass(selector + '-mouse-click');
                }
            );

            $(this).mouseup(
                function() {
                    $(this).removeClass(removeClassNames);
                    $(this).addClass(addClassName);
                    if (presenter.configuration.blockNotVisited) {
                        presenter.setDisabledPagesStyle();
                    }
                }
            );
        });
    }

    function handleElementHoverAndMouseDown() {
        var notSelectorsList = '.navigationbar-element-first,.navigationbar-element-last,' +
            '.navigationbar-element-next,.navigationbar-element-previous,.navigationbar-element-current';

        var iOS = ( navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false );

        presenter.$wrapper.find('.navigationbar-element').not(notSelectorsList).each(function() {
            if (!iOS) {
            	$(this).hover(
            		function() {
                    	$(this).removeClass('navigationbar-element');
                    	$(this).addClass('navigationbar-element-mouse-hover');
                	},
                	function() {
                    	$(this).removeClass('navigationbar-element-mouse-hover');
                    	$(this).addClass('navigationbar-element');
                    	 if (presenter.configuration.blockNotVisited) {
                            presenter.setDisabledPagesStyle();
                        }
                	}
            	);
            }

            $(this).mousedown(
                function() {
                    $(this).removeClass('navigationbar-element');
                    $(this).addClass('navigationbar-element-mouse-click');
                }
            );

            $(this).mouseup(
                function() {
                    $(this).removeClass('navigationbar-element-mouse-click');
                    $(this).addClass('navigationbar-element');
                    if (presenter.configuration.blockNotVisited) {
                        presenter.setDisabledPagesStyle();
                    }
                }
            );
        });
    }

    function removeAllElements() {
        presenter.$view.find("span[class*=navigationbar-element]").parent().remove();
    }

    function generateHomeArrowElement() {
        var isElementInactive = presenter.currentIndex === 0;
        var elementStyle = isElementInactive ? "navigationbar-element-first-inactive" : "navigationbar-element-first";

        if (presenter.$view.has('[class*="navigationbar-element-first"]').length &lt; 1) {
            var homeElementArrow = '&lt;a href="#"&gt;' +
                '&lt;span class="' + elementStyle + '"&gt;&amp;lt;&amp;lt;&lt;/span&gt;' +
                '&lt;/a&gt;';

            presenter.$wrapper.append(homeElementArrow);
        }
    }

    function generatePreviousArrowElement() {
        var homeElement = presenter.$view.find('[class*="navigationbar-element-first"]:first').parent();
        var isElementInactive = presenter.currentIndex === 0;
        var elementStyle = isElementInactive ? "navigationbar-element-previous-inactive inactive" : "navigationbar-element-previous";

        var previousElementArrow = '&lt;a href="#"&gt;&lt;span class="' + elementStyle + '"&gt;&amp;lt;&lt;/span&gt;&lt;/a&gt;';

        if (presenter.configuration.hideHomeLastArrows) {
            presenter.$wrapper.append(previousElementArrow);
        } else {
            homeElement.after(previousElementArrow);
        }

    }

    function generateNextArrowElement() {
        var previousElement = presenter.$view.find('[class*="navigationbar-element-previous"]:first').parent();
        var isElementInactive = presenter.currentIndex === presenter.pageCount - 1;
        var elementStyle = isElementInactive ? "navigationbar-element-next-inactive inactive" : "navigationbar-element-next";

        var nextElementArrow = '&lt;a href="#"&gt;&lt;span class="' + elementStyle + '"&gt;&amp;gt;&lt;/span&gt;&lt;/a&gt;';

        presenter.$wrapper.append(nextElementArrow);
    }

    function generateReportArrowElement() {
        var isElementInactive = presenter.currentIndex === presenter.pageCount - 1;
        var elementStyle = isElementInactive ? "navigationbar-element-last-inactive" : "navigationbar-element-last";

        if (presenter.$view.has('[class*="navigationbar-element-last"]').length &lt; 1) {
            var reportElementArrow = '&lt;a href="#"&gt;&lt;span class="' + elementStyle + '"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;/a&gt;';

            presenter.$wrapper.append(reportElementArrow);
        }
    }

    function generateHomeAndPreviousArrowsElements() {
        if (!presenter.configuration.hideHomeLastArrows) {
            generateHomeArrowElement();
        }

        if (presenter.configuration.showNextPrevArrows) {
            generatePreviousArrowElement();
        }
    }

    function generateReportAndNextArrowsElements() {
        if (presenter.configuration.showNextPrevArrows) {
            generateNextArrowElement();
        }

        if (!presenter.configuration.hideHomeLastArrows) {
            generateReportArrowElement();
        }
    }

    function generateDottedElement(dotsSide) {
        return '&lt;a href="#"&gt;' +
            '&lt;span class="navigationbar-element navigationbar-dotted-element ' + dotsSide.CSSClass + '"&gt;&amp;hellip;&lt;/span&gt;' +
            '&lt;/a&gt;';
    }

    // Index is displayed page number
    function generateIndexElementStub(index, navigationBarMoved) {
        var isCurrentElement, pageIndex;

        if(presenter.configuration.firstPageAsCover){
            isCurrentElement = !navigationBarMoved ? (index - 1 + 1) === presenter.currentIndex : (index - 1 + 1) === movedFromIndex;
            pageIndex = parseInt(index, 10) + 1;
        }else{
            isCurrentElement = !navigationBarMoved ? (index - 1) === presenter.currentIndex : (index - 1) === movedFromIndex;
            pageIndex = parseInt(index, 10);
        }

        var currentElementStyle = isCurrentElement ? "navigationbar-element-current" : "navigationbar-element";
        var elementId = '';
        if (isCurrentElement) {
            var addonID = presenter.configuration.ID;
            elementId = 'id="current-page-number' + (addonID ? '-' + addonID : '') + '" ';
        }

        return '&lt;a href="#"&gt;' +
            '&lt;span ' + elementId + 'class="' + currentElementStyle + ' navigationbar-indexed-element' +'" data-page-number="' + pageIndex + '"&gt;' + Internationalization.translate(index, presenter.configuration.language) + '&lt;/span&gt;' +
            '&lt;/a&gt;';
    }

    function generateIndexedElements(navigationBarMoved, triggerOnRun) {
        var firstElementSelector = presenter.configuration.showNextPrevArrows ? '[class*="navigationbar-element-previous"]' : '[class*="navigationbar-element-first"]';
        //var firstElement = presenter.$view.find(firstElementSelector).parent();

        var element; // Works as temporary indexed element
        var dottedElement; // Works as temporary dotted element

        var dotsLeftTargetIndex;
        var dotsRightTargetIndex;
        var n = 0;

        var pageCount;
        if(presenter.configuration.firstPageAsCover){
            pageCount = presenter.pageCount-1;
        }else{
            pageCount = presenter.pageCount;
        }

        if (maxElementCount &gt;= pageCount) { // All pages will be displayed
            if(presenter.configuration.lastPageSeparated){
                for (n = 1; n &lt;= pageCount; n++) {
                    if(n != pageCount){
                        element = generateIndexElementStub(n, navigationBarMoved);
                        presenter.$wrapper.append(element);
                    }
                }
                presenter.allPagesDisplayed = true;
            }else{
                for (n = 1; n &lt;= pageCount; n++) {
                    element = generateIndexElementStub(n, navigationBarMoved);
                    presenter.$wrapper.append(element);
                }
            }
            presenter.allPagesDisplayed = true;
        } else {
            var dottedElementCount = -1;
            if(presenter.configuration.firstPageAsCover &amp;&amp; triggerOnRun) {
                dottedElementCount = 0;
            }

            if (presenter.currentIndex &lt; (maxElementCount + dottedElementCount)) { // -1 for dotted element
                for (n = 0; n &lt; maxElementCount - 1; n++) {
                    element = generateIndexElementStub(n + 1, navigationBarMoved);
                    presenter.$wrapper.append(element);
                }

                // Dots are displayed on the right
                dotsRightTargetIndex = maxElementCount - 1;
                presenter.$wrapper.append(generateDottedElement(DOTTED_SIDE.RIGHT));
            } else if (presenter.currentIndex &gt; (presenter.pageCount - maxElementCount)) {
                // Dots are displayed on the left -&gt; -1 to max element count
                dotsLeftTargetIndex = (pageCount - 1) - (maxElementCount - 2) - 1;
                dottedElement = generateDottedElement(DOTTED_SIDE.LEFT);
                presenter.$wrapper.append(dottedElement);

                if(presenter.configuration.lastPageSeparated){
                    for (n = pageCount - maxElementCount; n &lt; pageCount; n++) {
                        if(presenter.configuration.lastPageSeparated &amp;&amp; n != (pageCount-1)){
                            element = generateIndexElementStub(n + 1, navigationBarMoved);
                            presenter.$wrapper.append(element);
                        }
                    }
                }else{
                    for (n = pageCount - maxElementCount + 1; n &lt; pageCount; n++) {
                        element = generateIndexElementStub(n + 1, navigationBarMoved);
                        presenter.$wrapper.append(element);
                    }
                }
            } else {
                var numberOfElement = maxElementCount - 2;

                var temp = presenter.currentIndex - (numberOfElement + 1);
                var multiplier = parseInt(temp / numberOfElement, 10);
                var startIndex = (numberOfElement + 1) + multiplier * numberOfElement;

                dotsLeftTargetIndex = startIndex - 1;
                dottedElement = generateDottedElement(DOTTED_SIDE.LEFT);
                presenter.$wrapper.append(dottedElement);

                if(startIndex == presenter.currentIndex &amp;&amp; presenter.configuration.firstPageAsCover) {
                    startIndex = startIndex - 1;
                }

                for (n = 0; n &lt; numberOfElement; n++) {
                    var indexedElement = generateIndexElementStub(startIndex + 1 + n, navigationBarMoved);
                    presenter.$wrapper.append(indexedElement);
                }

                dotsRightTargetIndex = startIndex + numberOfElement;
                presenter.$wrapper.append(generateDottedElement(DOTTED_SIDE.RIGHT));
            }
        }

        return {
            leftIndex: dotsLeftTargetIndex,
            rightIndex: dotsRightTargetIndex
        };
    }

    function generateElements(elementWidth, elementHeight, navigationBarMoved, preview, horizontalGap, triggerOnRun) {
        removeAllElements();

        generateHomeAndPreviousArrowsElements();

        var dotsIndexes = generateIndexedElements(navigationBarMoved, triggerOnRun);

        generateReportAndNextArrowsElements();

        if (!preview) {
            handleMouseActions(dotsIndexes.leftIndex, dotsIndexes.rightIndex, elementWidth, elementHeight, preview, horizontalGap);
        }

        if (presenter.$wrapper.css('direction') === 'rtl') {
            reorderElements(dotsIndexes, elementWidth, elementHeight, preview, horizontalGap);
        }

        if(presenter.configuration.lastPageSeparated &amp;&amp; presenter.allPagesDisplayed){
            var elementsCount = presenter.$view.find("span[class^=navigationbar-element]").length;
            var missingWidth = parseInt(elementWidth/elementsCount, 10);
            elementWidth = elementWidth + missingWidth;
        }

        presenter.$view.find("span[class^=navigationbar-element]").each(function () {
            var width = $(this).hasClass('navigationbar-element-last') ? elementWidth + horizontalGap : elementWidth;
            $(this).width(width + 'px');
            $(this).height(elementHeight + 'px');
            $(this).css('line-height', elementHeight + 'px');
        });

        if (presenter.configuration.blockNotVisited) {
            presenter.setDisabledPagesStyle();
        }

        return dotsIndexes;
    }

    function reorderElements(dotsIndexes, elementWidth, elementHeight, preview, horizontalGap) {
        var elements = [];

        presenter.$wrapper.children('a').each(function () {
            elements.push($(this));
            $(this).remove();
        });

        for (var i = elements.length - 1; i &gt;= 0; i--) {
            presenter.$wrapper.append(elements[i]);
        }

        if (!preview) {
            handleMouseActions(dotsIndexes.leftIndex, dotsIndexes.rightIndex, elementWidth, elementHeight, preview, horizontalGap);
        }

        if (presenter.configuration.blockNotVisited) {
            presenter.setDisabledPagesStyle();
        }
    }

    presenter.arePagesNamesCorrect = function (pageNames, length) {
        for (var i = 0; i &lt; pageNames.length; i++) {
            if (length &gt; 1 &amp;&amp; pageNames[i] == "") {
                return false;
            }
            if (isNaN(pageNames[i])) {
                switch (pageNames[i]) {
                    case "previous":
                        break;
                    case "first":
                        break;
                    case "last":
                        break;
                    case "next":
                        break;
                    default:
                        return false;
                }
            } else {
                if(length&gt;1) {
                    if (pageNames[i] % 1 !== 0 || pageNames[i] &lt;= 0) {
                        return false;
                    }
                }
            }
        }
        return true;
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeFrom_01(model);
        upgradedModel = presenter.upgradeFrom_02(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel['langAttribute']) {
            upgradedModel['langAttribute'] = '';
        }
         if (!upgradedModel['speechTexts']) {
            upgradedModel['speechTexts'] =
                {
                    DottedLeft: {DottedLeft: ''},
                    DottedRight: {DottedRight: ''},
                    GoToPageNumber: {GoToPageNumber: ''},
                    NextPage: {NextPage: ''},
                    PrevPage: {PrevPage: ''},
                    Titled: {Titled: ''}
                };
        }

        if (!upgradedModel['playTitle']) {
            upgradedModel['playTitle'] = '';
        }

        return upgradedModel;
    };

     presenter.upgradeFrom_02 = function (model) {
         var upgradedModel = {};
         $.extend(true, upgradedModel, model);

         if (!upgradedModel['blockNotVisited']) {
             upgradedModel['blockNotVisited'] = 'False';
         }
         if (!upgradedModel['speechTexts']['DisabledPage']) {
             upgradedModel['speechTexts']['DisabledPage'] = {DisabledPage: ''};
         }
         return upgradedModel;
     };

    presenter.validateModel = function (model) {
        var validatedModel = {
            isError: false,
            styles: model['Styles'],
            showNextPrevArrows: model.ShowNextPrevArrows === 'True',
            hideHomeLastArrows: model.HideHomeLastArrows === 'True',
            language: getLanguage(model),
            addClassNBPageOK: model.AddClassNBPageOK === 'True',
            ID: model.ID,
            firstPageAsCover: ModelValidationUtils.validateBoolean(model["firstPageAsCover"]),
            lastPageSeparated: ModelValidationUtils.validateBoolean(model["lastPageSeparated"]),
            langTag: model['langAttribute'],
            speechTexts: getSpeechTexts(model['speechTexts']),
            playTitle: 0 === model['playTitle'].toLowerCase().localeCompare('true'),
            blockNotVisited: ModelValidationUtils.validateBoolean(model["blockNotVisited"])
        };

        if (!model['Styles']) {
            return validatedModel;
        }

        for (var i = 0; i &lt; model['Styles'].length; i++) {
            var pages = model['Styles'][i].Pages;
            var pageNames = pages.trim().split(',');
            if (model['Styles'].length &gt; 1) {
                if (!pages) {
                    return returnErrorObject('E_01');
                }
                if (!model['Styles'][i]['Style'] &amp;&amp; !model['Styles'][i]['Class']) {
                    return returnErrorObject('E_01');
                }
            }
            if(!presenter.arePagesNamesCorrect(pageNames, model['Styles'].length)){
                return returnErrorObject('E_04');
            }
        }

        return validatedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function getSpeechTexts (speechTexts) {
        var speechTextsModel = {
            goToPageNumber:  'Go to page number',
            nextPage: 'Go to next page',
            prevPage: 'Go to previous page',
            titled: 'Titled',
            dottedRight: 'Show more pages',
            dottedLeft: 'Show earlier pages',
            disabledPage: 'Disabled page'
        };

        if (!speechTexts) {
            return speechTextsModel;
        }

        speechTextsModel = {
            goToPageNumber:    getSpeechTextProperty(speechTexts['GoToPageNumber']['GoToPageNumber'], speechTextsModel.goToPageNumber),
            nextPage: getSpeechTextProperty(speechTexts['NextPage']['NextPage'], speechTextsModel.nextPage),
            prevPage:  getSpeechTextProperty(speechTexts['PrevPage']['PrevPage'], speechTextsModel.prevPage),
            titled:     getSpeechTextProperty(speechTexts['Titled']['Titled'], speechTextsModel.titled),
            dottedRight:   getSpeechTextProperty(speechTexts['DottedRight']['DottedRight'], speechTextsModel.dottedRight),
            dottedLeft:      getSpeechTextProperty(speechTexts['DottedLeft']['DottedLeft'], speechTextsModel.dottedLeft),
            disabledPage:      getSpeechTextProperty(speechTexts['DisabledPage']['DisabledPage'], speechTextsModel.disabledPage),
        };
        return speechTextsModel;
    }

    presenter.getArrowsCount = function () {
        var arrowsCount = 0;

        if (!presenter.configuration.hideHomeLastArrows) arrowsCount += 2;
        if (presenter.configuration.showNextPrevArrows) arrowsCount += 2;

        return arrowsCount;
    };

    presenter.addAdditionalStyleToPage = function (page, styleName, styleValue, clazz) {
        if (isNaN(page)) {
            presenter.$wrapper.find("span[class^='navigationbar-element-"+ page +"']").css(styleName, styleValue);
            presenter.$wrapper.find("span[class^='navigationbar-element-"+ page +"']").addClass(clazz);

        } else {
            presenter.$wrapper.find("[data-page-number='" + page + "']").addClass(clazz);
            presenter.$wrapper.find("[data-page-number='" + page + "']").css(styleName, styleValue);
        }
    };

    presenter.setPageStyles = function() {
        $.each(presenter.configuration.styles, function() {
            var pages = this['Pages'].split(',');
            var cssStyle = this['Style'];
            var styles = cssStyle.split(';');

            for (var page = 0; page &lt; pages.length; page++) {
                var pageElement = pages[page].trim();
                for(var pageStyle = 0; pageStyle &lt; styles.length; pageStyle++) {
                    var oneStyle =  styles[pageStyle].split(':');
                    if(oneStyle[0]) {
                        oneStyle[0] = oneStyle[0].trim();
                    }
                    if(oneStyle[1]) {
                        oneStyle[1] = oneStyle[1].trim();
                    }
                    presenter.addAdditionalStyleToPage(pageElement, oneStyle[0], oneStyle[1], this['Class']);
                }
            }
        });
    };

    presenter.setDisabledPagesStyle = function() {
        if (!presenter.playerController) return;

        for (var page = 0; page &lt; presenter.pageCount; page++) {
            if (!presenter.visitedPages[page]) {
                presenter.addAdditionalStyleToPage(page+1, '', '', 'disabled');
            }
        }
        var currentIndex = 0;
        if (movedFromIndex) {
            currentIndex = movedFromIndex;
        } else {
            currentIndex = presenter.playerController.getCurrentPageIndex();
        }
        if (!presenter.visitedPages[0] || currentIndex == 0) {
            var $el = presenter.$wrapper.find('.navigationbar-element-first:first');
            $el.addClass('navigationbar-element-first-inactive disabled');
        }
        if (currentIndex &gt; 0 &amp;&amp; !presenter.visitedPages[currentIndex-1]) {
            var $el = presenter.$wrapper.find('.navigationbar-element-previous:first');
            $el.addClass('navigationbar-element-previous-inactive disabled');
        }
        if (currentIndex &lt; presenter.pageCount - 1 &amp;&amp; !presenter.visitedPages[currentIndex+1]) {
            var $el = presenter.$wrapper.find('.navigationbar-element-next:first');
            $el.addClass('navigationbar-element-next-inactive disabled');
        }
        if (!presenter.visitedPages[presenter.pageCount-1] || currentIndex == presenter.pageCount-1) {
            var $el = presenter.$wrapper.find('.navigationbar-element-last:first');
            $el.addClass('navigationbar-element-last-inactive disabled');
        }
    };

    function presenterLogic(view, model, isPreview) {
    	presenter.$view = $(view);
        presenter.$wrapper = presenter.$view.find('.navigationbar-wrapper:first');
        var $element = presenter.$view.find('.navigationbar-element-first');

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        var arrowsCount = presenter.getArrowsCount();

        if(presenter.configuration.isError){
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        if (isPreview) {
            presenter.currentIndex = 0;
            presenter.pageCount = 21;
        }

        DOMOperationsUtils.setReducedSize(presenter.$view, presenter.$wrapper);
        var elementDimensions = DOMOperationsUtils.getOuterDimensions($element),
            elementDistances = DOMOperationsUtils.calculateOuterDistances(elementDimensions);

        var elementBaseWidth = parseInt($element.width(), 10) + elementDistances.horizontal;
        maxElementCount = parseInt((presenter.$wrapper.width() - (arrowsCount * elementBaseWidth)) / elementBaseWidth, 10) - 4;
        var pageCount;
        if(presenter.configuration.firstPageAsCover){
            pageCount = presenter.pageCount-1;
        }else{
            pageCount = presenter.pageCount;
        }
        var numberOfElements = pageCount &lt; maxElementCount ? pageCount + arrowsCount : maxElementCount + arrowsCount;
        var elementWidth = parseInt(presenter.$wrapper.width() / numberOfElements  - elementDistances.horizontal, 10);
        var elementHeight = parseInt(presenter.$wrapper.height() - elementDistances.vertical, 10);
        var horizontalGap = presenter.$wrapper.width() - (elementWidth + elementDistances.horizontal) * numberOfElements;

        removeAllElements();

        generateElements(elementWidth, elementHeight, false, isPreview, horizontalGap, true);
        if(model['Styles']) {
            presenter.setPageStyles();
        }
        if (presenter.configuration.blockNotVisited &amp;&amp; !isPreview) {
            presenter.setDisabledPagesStyle();
        }
    }

    presenter.setShowErrorsMode = function(){
        presenter.isCurrentPageOk();
    };

    presenter.reset = function () {
        presenter.isCurrentPageOk();
        if (presenter.configuration.blockNotVisited) {
            reloadVisitedPages();
            presenter.setDisabledPagesStyle();
        }
    };

    presenter.getPercentageScore = function (pageIndex) {
        var id = presenter.presentation.getPage(pageIndex).getId();
        var pageScore = presenter.scoreService.getPageScoreById(id);

        return (pageScore.score/pageScore.maxScore) * 100;
    };

    presenter.isCurrentPageOk = function () {
        if(presenter.presentation.getPage(presenter.originalIndex).isReportable()){
            var percentageScore = presenter.getPercentageScore(presenter.originalIndex);
            var $page = presenter.$wrapper.find("[data-page-number='" + (presenter.originalIndex + 1) + "']");
            var id = presenter.presentation.getPage(presenter.originalIndex).getId();
            var pageScore = presenter.scoreService.getPageScoreById(id);

            if((percentageScore == 100 &amp;&amp; pageScore.errorCount == 0) || isNaN(percentageScore)){
                $page.addClass("navigationbar-page-ok");
                presenter.pagesOk.push(presenter.originalIndex + 1);
            }

            if(percentageScore &lt; 100 || pageScore.errorCount &gt; 0){
                $page.removeClass("navigationbar-page-ok");
                for(var k = presenter.pagesOk.length - 1; k &gt;= 0; k--) {
                    if(presenter.pagesOk[k] === (presenter.originalIndex + 1)) {
                        presenter.pagesOk.splice(k, 1);
                    }
                }
            }
        }
    };

    presenter.isPageOK = function () {
        var visibleElements = presenter.$view.find('.navigationbar-indexed-element');
        visibleElements.each( function () {
            var pageNumber = $(this).attr('data-page-number');
            if(presenter.presentation.getPage(pageNumber-1).isReportable() &amp;&amp; presenter.presentation.getPage(pageNumber-1).isVisited()){
              var percentageScore = presenter.getPercentageScore(pageNumber-1);
              var id = presenter.presentation.getPage(pageNumber-1).getId();
              var pageScore = presenter.scoreService.getPageScoreById(pageNumber-1);

              if(isNaN(percentageScore)){
                  percentageScore = 100;
              }

              if(percentageScore == 100 &amp;&amp; pageScore.errorCount == 0){
                  presenter.$wrapper.find("[data-page-number='" + pageNumber + "']").addClass("navigationbar-page-ok");
              }
            }
        });
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function (view, model) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();

        presenterLogic(view, model, false);

        presenter.pageLoaded.then(function() {
            presenter.isPageOK();
        });
    };
    
    presenter.onEventReceived = function(eventName, eventData) {
        if (eventName == 'PageLoaded') {
            presenter.currentIndex = presenter.playerController.getCurrentPageIndex();
            presenter.pageIndex = presenter.currentIndex;
            presenter.pageLoadedDeferred.resolve();
        }
        if (eventName == "ValueChanged" &amp;&amp; presenter.configuration.addClassNBPageOK &amp;&amp; !presenter.isShowAnswersActive) {
            presenter.currentIndex = presenter.pageIndex;
            presenter.isCurrentPageOk();
        }
        if(eventData.value == "resetClicked"){
            presenter.currentIndex = presenter.pageIndex;
            presenter.isCurrentPageOk();
        }

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }

        if (eventName == "visitedPagesUpdate") {
            presenter.visitedPagesUpdate();
        }
    };

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
    };

    presenter.hideAnswers = function () {
        presenter.isShowAnswersActive = false;
    };

    presenter.visitedPagesUpdate = function () {
    if (presenter.configuration.blockNotVisited) {
            reloadVisitedPages();
            presenter.$wrapper.find('.disabled').removeClass('disabled');
            if (presenter.currentIndex !== presenter.pageCount - 1) {
                presenter.$wrapper.find('.navigationbar-element-next-inactive').removeClass('navigationbar-element-next-inactive');
            }
            presenter.setDisabledPagesStyle();
        }
    }

    presenter.getState = function(){
        return JSON.stringify(presenter.state);
    };

    presenter.setState = function(state){
        if (state === null || state === "" || state === undefined) {
            return;
        }
        var parsedState = JSON.parse(state);
        if (parsedState.bookmarks != null) {
            presenter.state.bookmarks = parsedState.bookmarks;
        }
        presenter.refreshBookmarks();
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'bookmarkCurrentPage': presenter.bookmarkCurrentPage,
            'removeBookmark' : presenter.removeBookmark,
            'removeAllBookmarks' : presenter.removeallBookmarks
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.bookmarkCurrentPage = function() {
        var page = presenter.originalIndex + 1;
        if ( presenter.state.bookmarks.indexOf(page) == -1) {
            presenter.state.bookmarks.push(page);
        }
        presenter.refreshBookmarks();
    };

    presenter.removeBookmark = function() {
        var page = presenter.originalIndex + 1;
        var index = presenter.state.bookmarks.indexOf(page);
        if ( index != -1) {
            presenter.state.bookmarks.splice(index, 1);
        }
        presenter.refreshBookmarks();
    };

    presenter.removeAllBookmarks = function() {
        presenter.state.bookmarks = [];
        presenter.refreshBookmarks();
    };

    presenter.refreshBookmarks = function() {
        for (var i = 0; i &lt; presenter.pageCount; i++) {
            var page = i+1;
            if ( presenter.state.bookmarks.indexOf(page) != -1) {
                presenter.$wrapper.find("span[data-page-number='" + page + "']").addClass('bookmark');
            } else {
                presenter.$wrapper.find("span[data-page-number='" + page + "']").removeClass('bookmark');
            }

        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Next" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property isLocalized="true" name="Text" nameLabel="Next_property_text" type="string"/>
	</model>
<css>.next-wrapper,
.next-wrapper .next-container,
.next-wrapper .next-container .next-button {
    width: 100%;
    height: 100%;
}

.next-wrapper .next-container .next-button {
    background: url('resources/next-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}

.next-wrapper.disabled .next-container .next-button {
    cursor: not-allowed;
    background: url('resources/next-button-disabled.png') no-repeat center;
}
</css><view>&lt;div class="next-wrapper"&gt;
    &lt;div class="next-container"&gt;
        &lt;div class="next-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="next-wrapper"&gt;
    &lt;div class="next-container"&gt;
        &lt;div class="next-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonNext_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.wasSubmitted = false;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
        presenter.eventBus.addEventListener('Submitted', this);
    };

    presenter.onEventReceived = function(eventName) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        } else if (eventName == 'Submitted') {
            presenter.pageSubmittedDeferred.resolve();
        }
    };

    presenter.createEventData = function (additionalEventData) {
        var eventData = { 'source': presenter.configuration.addonID };
        for (var key in additionalEventData) {
            eventData[key] = additionalEventData[key];
        }
        return eventData;
    };

    presenter.sendEvent = function(eventName, eventData) {
        presenter.eventBus.sendEvent(eventName, eventData);
    };

    presenter.ERROR_CODES = {

    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model, isPreview) {
        var buttonText = model['Text'];

        return {
            'buttonText' : buttonText,
            'addonID' : model['ID']
        }
    };

    function getAllOfTheModulesThatImplementIsAttempted() {
        var pageIndex = presenter.playerController.getCurrentPageIndex(),
            ids = presenter.playerController.getPresentation().getPage(pageIndex).getModulesAsJS(),
            modules = [];

        $.each(ids, function() {
            var id = this.toString(),
                currentModule = presenter.playerController.getModule(id);

            if (currentModule &amp;&amp; currentModule.isAttempted !== undefined) {
                modules.push(currentModule);
            }

        });

        return modules;
    }

    function areAllModulesAttempted() {
        var areAllAttempted = true;

        $.each(presenter.modulesOnPage, function() {
            if (!this.isAttempted()) {
                areAllAttempted = false;
                return false; // break;
            }
        });

        return areAllAttempted;
    }

    presenter.goToNextPage = function() {
        var pageIndex = presenter.playerController.getCurrentPageIndex();
        if (pageIndex + 1 &lt; presenter.playerController.getPresentation().getPageCount()) {
            presenter.playerController.getCommands().gotoPageIndex(pageIndex + 1);
        }
    };

    function runLogic(view, model, isPreview) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();
        presenter.pageSubmittedDeferred = new $.Deferred();
        presenter.pageSubmitted = presenter.pageSubmittedDeferred.promise();
        presenter.runEndedDeferred = new $.Deferred();
        presenter.runEnded = presenter.runEndedDeferred.promise();

        presenter.configuration = presenter.validateModel(model, isPreview);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.$view = $(view);
        presenter.submitButton = presenter.$view.find('.next-button');
        presenter.submitButton.html(presenter.configuration.buttonText);
        presenter.nextWrapper = presenter.$view.find('.next-wrapper');
        presenter.nextWrapper.addClass('disabled');

        presenter.pageSubmitted.then(function() {
            presenter.nextWrapper.removeClass('disabled');
        });

        presenter.pageLoaded.then(function() {

            presenter.modulesOnPage = getAllOfTheModulesThatImplementIsAttempted();

            presenter.submitButton.click(function(e) {
                e.stopPropagation();

                if (presenter.nextWrapper.hasClass('disabled')) {

                    if (areAllModulesAttempted()) {

                        presenter.sendEvent('AllAttempted', presenter.createEventData());

                    } else {

                        presenter.sendEvent('NotAllAttempted', presenter.createEventData());

                    }

                } else {

                    presenter.wasSubmitted = true;
                    presenter.goToNextPage();

                }

            });

            presenter.runEndedDeferred.resolve();
        });

    }

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    presenter.setShowErrorsMode = function(){
    };

    presenter.setWorkMode = function(){
    };

    presenter.reset = function(){
    };

    presenter.getErrorCount = function(){
        return 0;
    };

    presenter.getMaxScore = function(){
        return 0;
    };

    presenter.getScore = function(){
        return 0;
    };

    presenter.getState = function(){
        return JSON.stringify({
            'wasSubmitted' : presenter.wasSubmitted
        });
    };

    presenter.setState = function(state){
        var parsed = JSON.parse(state);

        presenter.wasSubmitted = parsed.wasSubmitted;

        presenter.runEnded.then(function() {
            if (parsed.wasSubmitted) {
                presenter.nextWrapper.removeClass('disabled');
            }
        });
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Page_Counter" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Numericals" nameLabel="Page_Counter_property_numericals" type="{Western Arabic,Eastern Arabic,Perso-Arabic}"/>
		<property name="startFrom" nameLabel="Page_Counter_property_start_from" type="string"/>
		<property name="omittedPagesTexts" nameLabel="Page_Counter_property_omitted_pages_texts" type="list">
			<property name="pages" nameLabel="Page_Counter_property_pages" type="string"/>
			<property name="text" nameLabel="Page_Counter_property_text" type="string"/>
		</property>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Page_counter_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Page_counter_property_speech_texts" type="staticlist">

            <property name="Page" nameLabel="Page_Counter_property_page" type="staticrow">
                <property name="Page" nameLabel="Page_Counter_property_page" type="string"/>
            </property>

            <property name="OutOf" nameLabel="Page_Counter_property_out_of" type="staticrow">
                <property name="OutOf" nameLabel="Page_Counter_property_out_of" type="string"/>
            </property>

        </property>
	</model>
<css>.pagecounter {
    text-align: center;
    width: 100%;
    height: 100%;
    border: 0px solid black;
    padding: 1px;
    color: #7C818E;
    font-family: Georgia;
    font-size: 25px;
    direction: inherit;
}
</css><view>&lt;div class="pagecounter"&gt;
    &lt;!-- Here goes current page index / page count --&gt;
&lt;/div&gt;
</view><preview>&lt;div class="pagecounter"&gt;
    &lt;!-- Here goes current page index / page count --&gt;
&lt;/div&gt;
</preview><presenter>function AddonPage_Counter_create() {
    var presenter = function() { };
    var presentationController;
    var isWCAGOn = false;

    presenter.isCurrentlyVisible = true;

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
        }
    };
    
    presenter.hide = function() {
        presenter.isCurrentlyVisible = false;
        presenter.setVisibility(false);
    };
    
    presenter.show = function() {
        presenter.isCurrentlyVisible = true;
        presenter.setVisibility(true);
    };
    
    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };
    
    presenter.updateVisibility = function() {
        if (presenter.isCurrentlyVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };
    
    presenter.ERROR_CODES = {
            "L_01": "No language selected, have to be selected proper language in Numericals property",
            'ST_01': "Start from property have to be a positive integer.",
            "OPT_01": "Omitted pages texts have to be comma separated positive integers, greater than 0.",
            "OPT_02": "Omitted pages texts property can define only one text for one page.",
            "OPT_03": "Omitted pages texts property page number can't be greater than Start From property.",
            "OPT_04": "Omitted pages texts needs pages filled, not only texts",
            "OPT_05": "Omitted pages texts needs text filled, not only pages"
        };
    
    presenter.isPositiveInt = function (value) {
        if (value.trim() == "0") {
            return false;
        }

        if (ModelValidationUtils.isStringEmpty(value.trim()) == true) {
            return false;
        }

        var exp = /[0-9]/;
        for(var i = 0 ; i &lt; value.length; i++) {
            var character = value.charAt(i);
            if(!exp.test(character)) {
                return false;
            }
        }
        return true;
    };

    function getTextVoiceObject (text, lang) { return {text: text, lang: lang}; }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presentationController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }
    
    presenter.validateLanguage = function (model) {
        if (model.Numericals == 'Eastern Arabic') {
            return {isValid: true, value: Internationalization.EASTERN_ARABIC};
        }

        else if (model.Numericals == 'Perso-Arabic') {
            return {isValid: true, value: Internationalization.PERSO_ARABIC};
        }

        return {isValid: true, value: Internationalization.WESTERN_ARABIC};
    };

    function render(view, language, pageIndex, pageCount) {
        if (pageIndex &gt;= 0) {
            var viewContainer = $(view);
            var element = viewContainer.find(".pagecounter:first")[0];
            DOMOperationsUtils.setReducedSize(view, element);

            var addonText = Internationalization.translate((pageIndex + 1), language) + ' / ' + Internationalization.translate(pageCount, language);

            // This asures us that text will be center vertically
            $(element).css('line-height', $(element).height() + 'px');
            $(element).html(addonText);
        }
    }

    function renderText(view, text) {
        var viewContainer = $(view);
        var element = viewContainer.find(".pagecounter:first")[0];
        DOMOperationsUtils.setReducedSize(view, element);

        // This asures us that text will be center vertically
        $(element).css('line-height', $(element).height() + 'px');
        $(element).html(text);
    }


    presenter.validateStartFrom = function (startFrom) {
        if (ModelValidationUtils.isStringEmpty(startFrom)) {
            return { isValid: true, value: undefined};
        }

        if (!presenter.isPositiveInt(startFrom)) {
            return { isValid: false, errorCode: "ST_01"};
        }

        return {isValid: true, value: (parseInt(startFrom, 10) - 1)};
    };

    function parsePages(omittedPagesTextsObject) {
        var pages = omittedPagesTextsObject.pages;
        var text = omittedPagesTextsObject.text;

        if (ModelValidationUtils.isStringEmpty(pages) &amp;&amp; !ModelValidationUtils.isStringEmpty(text)) {
            return {isValid: false, errorCode: "OPT_04"};
        } else {
            if (ModelValidationUtils.isStringEmpty(text) &amp;&amp; !ModelValidationUtils.isStringEmpty(pages)) {
                return {isValid: false, errorCode: "OPT_05"};
            }
        }

        pages = pages.split(",");

        var parsedPages = [];
        for(var page = 0; page &lt; pages.length; page++) {
            var number = pages[page].trim();

            if(!presenter.isPositiveInt(number)) {
                return {isValid: false, errorCode: "OPT_01"};
            }

            parsedPages.push((parseInt(number, 10) - 1));
        }
        
        return {isValid: true, value: parsedPages};
    }

    presenter.validateOmittedPagesTexts = function (model, validatedStartFrom) {
        var omittedPagesTexts = model.omittedPagesTexts;

        if (omittedPagesTexts.length == 1 &amp;&amp;
            ModelValidationUtils.isStringEmpty(omittedPagesTexts[0].pages) &amp;&amp;
            ModelValidationUtils.isStringEmpty(omittedPagesTexts[0].text)) {

            return {isValid: true, value: {}};
        }

        var parsedOPT = {};
        
        for(var i = 0; i &lt; (omittedPagesTexts).length; i++) {
            var parsedPages = parsePages(omittedPagesTexts[i]);
            if(!parsedPages.isValid) {
                return parsedPages;
            }
            
            for(var page = 0; page &lt; parsedPages.value.length; page++) {
                if(parsedOPT[parsedPages.value[page]] != undefined) {
                    return {isValid: false, errorCode: "OPT_02"};
                }


                if(parsedPages.value[page] &gt;= validatedStartFrom) {
                    return {isValid: false, errorCode: "OPT_03"};
                }
                
                parsedOPT[parsedPages.value[page]] = omittedPagesTexts[i].text;
            }
            
        }
        
        return {isValid: true, value: parsedOPT};
    };
    
    presenter.validateModel = function(model) {
        var validatedStartFrom = presenter.validateStartFrom(model.startFrom);
        if (!validatedStartFrom.isValid) {
            return validatedStartFrom;
        }

        var validatedOmittedPagesTexts = presenter.validateOmittedPagesTexts(model, validatedStartFrom.value);
        if (!validatedOmittedPagesTexts.isValid) {
            return validatedOmittedPagesTexts;
        }

        return {
            isValid: true,
            ID: model.ID,
            startFrom: validatedStartFrom.value,
            omittedPagesTexts: validatedOmittedPagesTexts.value,
            Numericals: presenter.validateLanguage(model).value,
            langTag: model['langAttribute'],
            speechTexts: presenter.getSpeechTexts(model['speechTexts']),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"])
        };
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.getSpeechTexts = function(speechTexts) {
        var speechTextsModel = {
            page:  'Page',
            outOf: 'Out of'
        };

        if (!speechTexts) {
            return speechTextsModel;
        }

        speechTextsModel = {
            page: getSpeechTextProperty(speechTexts['Page']['Page'], speechTextsModel.page),
            outOf: getSpeechTextProperty(speechTexts['OutOf']['OutOf'], speechTextsModel.outOf)
        };

        return speechTextsModel;
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeStartFromAndOPT(model);
        return presenter.upgradeAddTTS(upgradedModel);
    };

    presenter.upgradeStartFromAndOPT = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.startFrom == undefined) {
            upgradedModel["startFrom"] = "";
        }

        if(model.omittedPagesTexts == undefined) {
            upgradedModel["omittedPagesTexts"] = [{"pages": "", "text": ""}];
        }

        return upgradedModel;
    };

    presenter.upgradeAddTTS = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.langAttribute == undefined) {
            upgradedModel['langAttribute'] = '';
        }

        if(model.speechTexts == undefined) {
            upgradedModel['speechTexts'] = {
                Page: {Page: ''},
                OutOf: {OutOf: ''}
            };
        }

        return upgradedModel;
    };
    
    function presenterLogic(view, model, isPreview) {
        presenter.$view = $(view);
        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);
        presenter.configuration = validatedModel;
        presenter.isCurrentlyVisible = presenter.configuration.isVisible;
        if (!isPreview) {
            presenter.updateVisibility();
        }
        
        if (!validatedModel.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.errorCode);
            return;
        }

        var language = validatedModel.Numericals;

        if (isPreview) {
            render(view, language, 0, 5);
        } else {
            var currentPageIndex = presentationController.getCurrentPageIndex();
            var pageCount = presentationController.getPresentation().getPageCount();

            if (validatedModel.startFrom) {
                var modifiedPageIndex = currentPageIndex - validatedModel.startFrom;
                pageCount = pageCount - validatedModel.startFrom;

                if (validatedModel.omittedPagesTexts[currentPageIndex]) {
                    renderText(view, validatedModel.omittedPagesTexts[currentPageIndex]);
                } else{
                    render(view, language, modifiedPageIndex, pageCount);
                }

            } else {
                render(view, language, currentPageIndex, pageCount);
            }
        }
    }

    presenter.setPlayerController = function(controller) {
        presentationController = controller;
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model) {
        presenterLogic(view, model, false);
    };

    presenter.keyboardController = function(keyCode, isShift, event) {
        if (keyCode == 13 || keyCode == 32) {
            if(event!== null &amp;&amp; event !== undefined) {
                event.preventDefault();
            }
            presenter.clickHandler();
        }
    };

    presenter.clickHandler = function() {
        presenter.playContent();
    };

    presenter.playContent = function() {
        var currentPageIndex = presentationController.getCurrentPageIndex();
        var pageCount = presentationController.getPresentation().getPageCount();
        var TextVoices = [];
        if (presenter.configuration.startFrom) {
            var modifiedPageIndex = currentPageIndex - presenter.configuration.startFrom;
            pageCount = pageCount - presenter.configuration.startFrom;
            if (presenter.configuration.omittedPagesTexts[currentPageIndex]) {
                TextVoices.push(getTextVoiceObject(presenter.configuration.omittedPagesTexts[currentPageIndex], presenter.configuration.langTag));
            } else{
                if(modifiedPageIndex &lt; 0) {
                    return;
                }
                var text = presenter.configuration.speechTexts.page + ' ';
                text += (modifiedPageIndex + 1) + ' ';
                text += presenter.configuration.speechTexts.outOf + ' ';
                text += presenter.getPageCount(pageCount);
                TextVoices.push(getTextVoiceObject(text));
            }

        } else {
            var text = presenter.configuration.speechTexts.page + ' ';
            text += (currentPageIndex + 1) + ' ';
            text += presenter.configuration.speechTexts.outOf + ' ';
            text += presenter.getPageCount(pageCount);
            TextVoices.push(getTextVoiceObject(text));
        }
        speak(TextVoices);
    };

    presenter.getPageCount = function(pageCount) {
        var lessonLangTag = document.documentElement.lang;
        if(0 === lessonLangTag.toLowerCase().localeCompare('pl') || 0 === lessonLangTag.toLowerCase().localeCompare('pl-pl')){
            if(pageCount===2) {
                return 'dwóch';
            }
            if(pageCount===3) {
                return 'trzech';
            }
            if(pageCount===4) {
                return 'czterech';
            }
        }
        return pageCount;
    };

    presenter.reset = function() {
        presenter.isCurrentlyVisible = presenter.configuration.isVisible;
        presenter.updateVisibility();
    };
    
    presenter.getState = function(){
        return JSON.stringify({
            visible : presenter.isCurrentlyVisible
        });
    };

    presenter.setState = function(state){
        if (state === undefined || state === '') {
             presenter.isCurrentlyVisible = true;
        } else {
             var parsedState = JSON.parse(state);
             presenter.isCurrentlyVisible = parsedState.visible;
        }

        presenter.updateVisibility();
    };
    
    presenter.isEnterable = function() {
        return false;
    };

    return presenter;
}
</presenter></addon><addon id="Page_Name">
	<model>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
	</model>
<css>.pagename {
    width: 100%;
    height: 100%;
    border: 0;
    padding: 1px;
    color: #7C818E;
    font-size: 25px;
    direction: inherit;
}

.printable_pagename_wrapper {
    text-align: center;
    font-size: 25px;
}
</css><view>&lt;div class="pagename"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="pagename"&gt;
&lt;/div&gt;
</preview><presenter>function AddonPage_Name_create() {
    var presenter = function () {};
    var presentationController;
    var printableController;

    function presenterLogic(view, name) {
        var nameElement = $(view).find('.pagename')[0];

        $(nameElement).text(name);
    }

    presenter.createPreview = function (view, model) {
        presenterLogic(view, "Page Name");
    };

    presenter.getPageName = function () {
        var presentation = presentationController.getPresentation();
        var currentPage = presentationController.getCurrentPageIndex();

        return presentation.getPage(currentPage).getName();
    };


    presenter.run = function (view, model) {
        var pageName = this.getPageName();

        presenterLogic(view, pageName);
    };

    presenter.setPlayerController = function(controller) {
        presentationController = controller;
    };

    presenter.setPrintableController = function (controller) {
        printableController = controller;
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var $view = $("&lt;div&gt;&lt;/div&gt;");
        $view.attr("id", model.ID);
        $view.addClass("printable_addon_pagename");

        var $wrapper = $("&lt;div&gt;&lt;/div&gt;");
        $wrapper.addClass("printable_pagename_wrapper");
        $wrapper.html(printableController.getPageName());

        
        $view.append($wrapper);
        return $view[0].outerHTML;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Page_Progress_Panel" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property displayName="Show Progress Bar" name="Show_Progress_Bar" nameLabel="Page_Progress_Panel_property_show_progress_bar" type="boolean"/>
		<property displayName="Progress Bar text" name="Progress_Bar_text" nameLabel="Page_Progress_Panel_property_progress_bar_text" type="string"/>
		<property displayName="Show Correct Answers" name="Show_Correct_Answers" nameLabel="Page_Progress_Panel_property_show_correct_answer" type="boolean"/>
		<property displayName="Correct Answers text" name="Correct_Answers_text" nameLabel="Page_Progress_Panel_property_correct_answers_text" type="string"/>
		<property displayName="Show All Answers" name="Show_All_Answers" nameLabel="Page_Progress_Panel_property_show_all_answers" type="boolean"/>
		<property displayName="All Answers text" name="All_Answers_text" nameLabel="Page_Progress_Panel_property_all_answers_text" type="string"/>
		<property displayName="Show Mistakes" name="Show_Mistakes" nameLabel="Page_Progress_Panel_property_show_mistakes" type="boolean"/>
		<property displayName="Mistakes text" name="Mistakes_text" nameLabel="Page_Progress_Panel_property_mistakes_text" type="string"/>
		<property displayName="Show Checks" name="Show_Checks" nameLabel="Page_Progress_Panel_property_show_checks" type="boolean"/>
		<property displayName="Checks text" name="Checks_text" nameLabel="Page_Progress_Panel_property_checks_text" type="string"/>
		<property displayName="Show Errors" name="Show_Errors" nameLabel="Page_Progress_Panel_property_show_errors" type="boolean"/>
		<property displayName="Errors text" name="Errors_text" nameLabel="Page_Progress_Panel_property_errors_text" type="string"/>
	</model>
<css>.page-progress-panel-container .progress-bar-container.hidden, .page-progress-panel-container .progress-box.hidden {
    display: none;
}

.page-progress-panel-container .progress-bar-container {
    width: 200px;
    height: 50px;
    position: relative;
    border-radius: 5px;
    border: 2px solid #02789F;
    padding: 2px;
    display: inline-block;
    float: left;
}

.page-progress-panel-container .progress-bar-container .text{
    min-height: 30px;
    text-align: center;
    font-size: 12px;
}


.page-progress-panel-container .progress-bar {
    background-color: #3CC6CD;
    position: absolute;
    border-radius: 5px;
    height: 50px;
}

.page-progress-panel-container .progress-text {
    width: 100%;
    text-align: center;
    font-size: 12px;
    position: inherit;
}

.page-progress-panel-container .text {
    width: 100%;
    text-align: center;
    font-size: 12px;
    position:inherit;
}

.page-progress-panel-container .progress-box .text {
    min-height: 35px;
}

.page-progress-panel-container .progress-box {
    line-height: 14px;
    display: inline-block;
    width: 50px;
    height: 50px;
    padding: 3px;
    border: 1px solid #02789F;
    margin-left: 5px;
    text-align: center;
    border-radius: 5px;
    font-size: 10px;
    float: left;
}
</css><view>&lt;div class="page-progress-panel-container"&gt;
    &lt;div class="progress-bar-container hidden"&gt;
        &lt;div class="progress-bar" style="width: 0%;"&gt;&lt;/div&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="progress-text"&gt;0%&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box correct-answers hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box max-score hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box mistakes hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box checks hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box errors hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="page-progress-panel-container"&gt;
    &lt;div class="progress-bar-container hidden"&gt;
        &lt;div class="progress-bar" style="width: 50%;"&gt;&lt;/div&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="progress-text"&gt;50%&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box correct-answers hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box max-score hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box mistakes hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box checks hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="progress-box errors hidden"&gt;
        &lt;div class="text"&gt;&lt;/div&gt;
        &lt;div class="value"&gt;0&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonPage_Progress_Panel_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

	presenter.configuration = {
		showProgressBar: false,
		showChecks: false,
		showErrors: false,
		showMistakes: false,
		showMaxScore: false,
		showCorrectAnswers: false,
		isVisible: true
	};

	presenter.state = {
		isVisible: true
	};

	presenter.lastScores = {
		progress: 0,
        sumOfMaxScore: 0,
		sumOfMistakes: 0,
		sumOfErrors: 0,
		sumOfChecks: 0,
		sumOfScores: 0
	};

    presenter.ERROR_CODES = {
    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        return {
            addonID: model['ID'],
            showProgressBar: ModelValidationUtils.validateBoolean(model['Show_Progress_Bar']),
			progressBarText: model['Progress_Bar_text'],
            showChecks: ModelValidationUtils.validateBoolean(model['Show_Checks']),
			checksText: model['Checks_text'],
            showErrors: ModelValidationUtils.validateBoolean(model['Show_Errors']),
			errorsText: model['Errors_text'],
            showMistakes: ModelValidationUtils.validateBoolean(model['Show_Mistakes']),
			mistakesText: model['Mistakes_text'],
            showMaxScore: ModelValidationUtils.validateBoolean(model['Show_All_Answers']),
			maxScoreText: model['All_Answers_text'],
            showCorrectAnswers: ModelValidationUtils.validateBoolean(model['Show_Correct_Answers']),
			correctAnswersText: model['Correct_Answers_text'],
			isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"])
        }
    };

    function runLogic(view, model, isPreview) {
        presenter.$view = $(view);
		presenter.view = view;
        presenter.$progressBarContainer = presenter.$view.find('.progress-bar-container');
        presenter.$progressBar = presenter.$view.find('.progress-bar');
        presenter.$progressText = presenter.$view.find('.progress-text');
        presenter.$checks = presenter.$view.find('.checks');
        presenter.$errors = presenter.$view.find('.errors');
        presenter.$mistakes = presenter.$view.find('.mistakes');
        presenter.$maxScore = presenter.$view.find('.max-score');
        presenter.$correctAnswers = presenter.$view.find('.correct-answers');
        presenter.configuration = presenter.validateModel(model);
		presenter.displayText();
		presenter.state.isVisible = presenter.configuration.isVisible;

        removeHidden(presenter.configuration.showProgressBar, presenter.$progressBarContainer);
        removeHidden(presenter.configuration.showChecks, presenter.$checks);
        removeHidden(presenter.configuration.showErrors, presenter.$errors);
        removeHidden(presenter.configuration.showMistakes, presenter.$mistakes);
        removeHidden(presenter.configuration.showMaxScore, presenter.$maxScore);
        removeHidden(presenter.configuration.showCorrectAnswers, presenter.$correctAnswers);
		if (!isPreview) {
		    presenter.updateVisibility();
			var currentPageIndex = presenter.playerController.getCurrentPageIndex();
			var pageId = presenter.playerController.getPresentation().getPage(currentPageIndex).getId();
			var score = presenter.playerController.getScore().getPageScoreById(pageId);
			presenter.lastScores.sumOfMaxScore = score.maxScore;
			presenter.displayScores(presenter.lastScores);
			presenter.view.addEventListener('ShowAnswers', this);
		}

    };

	presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
			presenter.showAnswers();
		}
    };

	presenter.showAnswers = function() {
		var currentPageIndex = presenter.playerController.getCurrentPageIndex();
		var pageId = presenter.playerController.getPresentation().getPage(currentPageIndex).getId();
		var score = presenter.playerController.getScore().getPageScoreById(pageId);
		presenter.lastScores.sumOfMistakes = score.mistakeCount;
		presenter.lastScores.sumOfChecks = score.checkCount;
		presenter.displayScores(presenter.lastScores);
	};

    function removeHidden(shouldRemove, $element) {
        if (shouldRemove) {
            $element.removeClass('hidden');
        } else {
            $element.addClass('hidden');
        }
    };

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    function getPageScore(){
        var currentPageIndex = presenter.playerController.getCurrentPageIndex();
		var page = presenter.playerController.getPresentation().getPage(currentPageIndex);
        var pageId = page.getId();
        var score = presenter.playerController.getScore().getPageScoreById(pageId);

		if (!page.isReportable()) {
			var percentageScore = 0;
		} else if (score.maxScore &gt; 0) {
            var percentageScore = (score.score*100.0) / score.maxScore;
        }
        return {
            progress: Math.round(percentageScore),
            sumOfMaxScore: score.maxScore,
            sumOfMistakes: score.mistakeCount,
            sumOfErrors: score.errorCount,
            sumOfChecks: score.checkCount,
            sumOfScores: score.score
        };
    }

	presenter.displayText = function() {
		if (presenter.configuration.showProgressBar) {
            presenter.$progressBarContainer.find('.text').html(presenter.configuration.progressBarText);
        }

        if (presenter.configuration.showChecks) {
            presenter.$checks.find('.text').html(presenter.configuration.checksText);
        }

        if (presenter.configuration.showMistakes) {
            presenter.$mistakes.find('.text').html(presenter.configuration.mistakesText);
        }

        if (presenter.configuration.showErrors) {
            presenter.$errors.find('.text').html(presenter.configuration.errorsText);
        }

        if (presenter.configuration.showMaxScore) {
            presenter.$maxScore.find('.text').html(presenter.configuration.maxScoreText);
        }

        if (presenter.configuration.showCorrectAnswers) {
            presenter.$correctAnswers.find('.text').html(presenter.configuration.correctAnswersText);
        }
	};

	presenter.displayScores = function (pageScore) {
		if (presenter.configuration.showProgressBar) {
            presenter.$progressBar.css('width', pageScore.progress + '%');
            presenter.$progressText.html(pageScore.progress + '%');
        }

        if (presenter.configuration.showChecks) {
            presenter.$checks.find('.value').html(pageScore.sumOfChecks);
        }

        if (presenter.configuration.showMistakes) {
            presenter.$mistakes.find('.value').html(pageScore.sumOfMistakes);
        }

        if (presenter.configuration.showErrors) {
            presenter.$errors.find('.value').html(pageScore.sumOfErrors);
        }

        if (presenter.configuration.showMaxScore) {
            presenter.$maxScore.find('.value').html(pageScore.sumOfMaxScore);
        }

        if (presenter.configuration.showCorrectAnswers) {
            presenter.$correctAnswers.find('.value').html(pageScore.sumOfScores);
        }
	}
    presenter.setShowErrorsMode = function(){
        presenter.lastScores = getPageScore();
		presenter.displayScores(presenter.lastScores);
    };

    presenter.setWorkMode = function(){
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        (presenter.state.isVisible) ? presenter.show() : presenter.hide();
    };
	
    presenter.updateMistakes = function() {
	presenter.lastScores.sumOfMistakes = getPageScore().sumOfMistakes;
	if (presenter.configuration.showMistakes) {
		presenter.$mistakes.find('.value').html(presenter.lastScores.sumOfMistakes);
	}
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show' : presenter.show,
            'hide' : presenter.hide,
	    'updateMistakes' : presenter.updateMistakes,
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.reset = function(){
		var currentPageIndex = presenter.playerController.getCurrentPageIndex();
        var pageId = presenter.playerController.getPresentation().getPage(currentPageIndex).getId();
        var score = presenter.playerController.getScore().getPageScoreById(pageId);
		presenter.lastScores = {
			progress: 0,
            sumOfMaxScore: score.maxScore,
            sumOfMistakes: score.mistakeCount,
            sumOfErrors: 0,
            sumOfChecks: score.checkCount,
            sumOfScores: 0
		};
		presenter.displayScores(presenter.lastScores);
		presenter.state.isVisible = presenter.configuration.isVisible;
		presenter.updateVisibility();
    };

    presenter.getState = function(){
		return JSON.stringify({
            progress: presenter.lastScores.progress,
            sumOfMaxScore: presenter.lastScores.sumOfMaxScore,
            sumOfMistakes: presenter.lastScores.sumOfMistakes,
            sumOfErrors: presenter.lastScores.sumOfErrors,
            sumOfChecks: presenter.lastScores.sumOfChecks,
            sumOfScores: presenter.lastScores.sumOfScores,
			isVisible: presenter.state.isVisible
        });
    };
    presenter.setState = function(state){
		var parsedState = JSON.parse(state);
		presenter.lastScores = {
			progress: parsedState.progress,
			sumOfMaxScore: parsedState.sumOfMaxScore,
			sumOfMistakes: parsedState.sumOfMistakes,
			sumOfErrors: parsedState.sumOfErrors,
			sumOfChecks: parsedState.sumOfChecks,
			sumOfScores: parsedState.sumOfScores
		};
		presenter.state.isVisible = parsedState.isVisible;
		presenter.updateVisibility();
		presenter.displayScores(presenter.lastScores);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Page_Rating" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Title Text" nameLabel="Page_Rating_property_title_text" type="string"/>
		<property name="Comment Text" nameLabel="Page_Rating_property_comment_text" type="string"/>
		<property name="Rates" nameLabel="Page_Rating_property_rates" type="list">
            <property name="Deselected" nameLabel="Page_Rating_property_deselected" type="image"/>
		    <property name="Selected" nameLabel="Page_Rating_property_selected" type="image"/>
		</property>
        <property isLocalized="true" name="Close button visible" nameLabel="Page_Rating_property_close_button_visible" type="boolean"/>
	</model>
<css>.page-rating-wrapper {
	border-style: solid;
	border-color: black;
    background-color: #ffffff;
	height: 100%;
	width: 100%;
}

.page-rating-comment &gt; [data-name="textarea"] {
    width: 65%;
	border: 2px solid #bfbab5;
	border-radius: 10px;
    padding: 3px;
	height: 60px;
	overflow: hidden;
    resize: none;
    float: left;
    margin-left: 10px;
    margin-bottom: 10px;
}

.page-rating-comment &gt; [data-name="textarea"]:focus {
	border-color:#333; 
}

.page-rating-comment{
    display: block;
	position: absolute;
	text-align: center;
	padding-top: 10px;
	bottom: 0;
	background-color: #f2f1d5;
	width: 100%;
}

.page-rating-comment &gt; p {
    padding-bottom: 3px;
    position: relative;
    text-align: left;
    margin-left: 15px;
}

.page-rating-rates &gt; img {
	height: 80px;
	width: 80px;
    margin: 2px;
    border-radius: 5px;
}

.page-rating-rates {
	text-align: center;
	bottom: 10px;
    padding: 3px;
}

.page-rating-title {
	background-color: #e1f2d5;
    padding-bottom: 3px;
    text-align: center;
}

.page-rating-comment &gt; .page-rating-submit-button {
    display: block;
    padding: 10px 15px;
    float: right;
    background: #4479BA;
    color: #FFF;
    margin-top: 28px;
    margin-right: 10px;
    margin-left: 10px;
    margin-bottom: 10px;
    border-radius: 4px;
    border: solid 1px #20538D;
    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 1px
}

.page-rating-comment &gt; .page-rating-submit-button:disabled {
    background-color: grey;
}

.page-rating-comment &gt; button:hover{
    background: #356094;
    border: solid 1px #2A4E77;
    text-decoration: none;
}

.page-rating-wrapper &gt; .page-rating-close-button {
    float: right;
    background: #4479BA;
    color: #FFF;
    margin-top: 2px;
    margin-right: 2px;
    border-radius: 4px;
    border: solid 1px #20538D;
    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 1px
}
</css><view>&lt;div class="page-rating-wrapper"&gt;
    &lt;div class="page-rating-title"&gt;&lt;/div&gt;
    &lt;div class="page-rating-rates"&gt;&lt;/div&gt;
    &lt;div class="page-rating-comment"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="page-rating-wrapper"&gt;
    &lt;div class="page-rating-title"&gt;&lt;/div&gt;
    &lt;div class="page-rating-rates"&gt;&lt;/div&gt;
    &lt;div class="page-rating-comment"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonPage_Rating_create() {
    var presenter = function () { };
    var eventBus;
    var selected_img = [],
    	deselected_img = [];
    
    presenter.currentRate = 0;
    presenter.playerController = null;
	presenter.isElementSelected = null;
	presenter.isModelError = false;
    presenter.addonID = null;
    presenter.originalDisplay = "";
    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };
	
    presenter.ERROR_CODES = {
        'E_01': "You have to add at least 2 rates.",
        'E_02': "You did not add Selected or/and Deselected image for at least one rate."
    };
    
    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.sanitizeModel = function (model)  {
        if(model.Rates.length &lt;2){
            return returnErrorObject('E_01');
        }

    	for (var model_img=0; model_img &lt; model.Rates.length; model_img++){
    		selected_img[model_img] = model.Rates[model_img].Selected;
			deselected_img[model_img] = model.Rates[model_img].Deselected;
    	}
        if(model.Rates.length &gt; 1) {
            for (var img = 0; img &lt; model.Rates.length; img++) {
                if (!model.Rates[img].Selected || !model.Rates[img].Deselected) {
                    return returnErrorObject('E_02');
                }
            }
        }
        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        var buttonCloseVisible = ModelValidationUtils.validateBoolean(model['Close button visible']);

        return {
            isError: false,
            rates: {
                selected: selected_img,
                deselected: deselected_img
            },
            length: model.Rates.length,
            title: model['Title Text'],
            comment: model['Comment Text'],
            isVisible: isVisible,
            closeButtonVisible: buttonCloseVisible
        }
    };

    function submitEventHandler (e) {
        e.stopPropagation();
        presenter.hide();
    }

    presenter.createRatingEventData = function (data) {
        return {
            source : presenter.addonID,
            item : data.index,
            value : data.selected ? "1" : "0"
        };
    };
    
    function clickEventHandler (e) {
        e.stopPropagation();

    	var $image = $(this),
            index = parseInt($image.data('index'), 10),
    		eventData = presenter.createRatingEventData({'index' : index+1, 'selected' : $image.attr("name") === "deselected"});
    	
    	if($image.attr("name") === "deselected") {
    		presenter.currentRate = index+1;
    	} else {
    		presenter.currentRate = 0;
    	}
    	eventBus.sendEvent('ValueChanged', eventData);

    	if( $image.attr("name") === "deselected" ) {
        	if(presenter.isElementSelected !== null) {
                var $selectedImage = presenter.$view.find('img[data-index="'+ presenter.isElementSelected +'"]');

                $selectedImage.attr({
                    'src': deselected_img[presenter.isElementSelected],
                    'name': "deselected"
                });
            }
              presenter.setSelectedImage(index);
        } else {
        	if(presenter.isElementSelected === index) {
                $image.attr({
                    "src": deselected_img[index],
                    "name": "deselected"
                });

                presenter.isElementSelected = null;
                presenter.$view.find('.page-rating-submit-button').attr('disabled','disabled');
        	}
        }
        
    }
    
    function updateTitle (view, title) {
    	if(title){
        	$(view).find('.page-rating-title').html(title);
        }
    }
    
    function updateComment(view, comment, isPreview){
        $(view).find('.page-rating-comment').prepend('&lt;p class="CommentText"&gt;&lt;/p&gt;');
        $(view).find('.CommentText').html(comment);
        $(view).find('.page-rating-comment').append('&lt;textarea&gt;&lt;/textarea&gt;');
        $(view).find('textarea').attr('data-name', 'textarea');
        $(view).find('.page-rating-comment').append('&lt;button type="button" class="page-rating-submit-button"&gt;Submit&lt;/button&gt;');
        $(view).find('.page-rating-submit-button').attr('disabled','disabled');

        if(!isPreview) {
            $(view).find(".page-rating-submit-button").live("click", submitEventHandler);
        }
    }
    
    function updateRates(view, rates, length,isPreview){
        if(rates){
            for (var i=0; i&lt;length; i++){
                var $image = $(document.createElement('img'));

                $image.attr({
                    'src': deselected_img[i],
                    'name': "deselected",
                    'data-index': i
                });

                $(view).find('.page-rating-rates').append($image);
            }
            if(!isPreview){
                $(view).find("img").live("click", clickEventHandler);
            }
        }
    }

    presenter.updateView = function (isPreview){
        if(presenter.configuration.closeButtonVisible){
            presenter.$view.find('.page-rating-wrapper').prepend('&lt;button type="button" class="page-rating-close-button"&gt;Close&lt;/button&gt;');
            presenter.$view.find(".page-rating-close-button").live("click", submitEventHandler);
        }
        updateTitle(presenter.$view, presenter.configuration.title);
        updateRates(presenter.$view, presenter.configuration.rates, presenter.configuration.length, isPreview);
        updateComment(presenter.$view, presenter.configuration.comment, isPreview);
        presenter.$view.css('display',presenter.originalDisplay);
    };

    presenter.presenterLogic = function (view, model, isPreview) {
    	presenter.$view = $(view);
    	presenter.configuration = presenter.sanitizeModel(model);

        if(presenter.configuration.isError){
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.originalDisplay = $(view).css('display');

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        presenter.updateView(isPreview);
    };

    presenter.run = function (view, model) {
        eventBus = presenter.playerController.getEventBus();
        presenter.presenterLogic(view, model, false);
        presenter.addonID = model.ID;
    };
    
    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.setSelectedImage = function(index){
        var $img_RateIndex = presenter.$view.find('img[data-index="'+ index +'"]');
        $img_RateIndex.attr({
            'src': selected_img[index],
            'name': 'selected'
        });

        presenter.isElementSelected = index;

        if(index == null){
            presenter.$view.find('.page-rating-submit-button').attr('disabled', 'disabled');
        }else {
            presenter.$view.find('.page-rating-submit-button').removeAttr('disabled');
        }
    };

    presenter.setCommentValue = function(comment){
        presenter.$view.find('textarea[data-name="textarea"]').text(comment);
    };

    presenter.getCommentValue = function(){
        return presenter.$view.find('textarea[data-name="textarea"]').val();
    };

    presenter.getState = function () {
        if (presenter.configuration.isError) {
            return "";
        }

    	return JSON.stringify({
    		commentValue: presenter.getCommentValue(),
    		isVisible: presenter.configuration.isVisible,
    		selectedItem: presenter.isElementSelected,
    		currentRate: presenter.currentRate
        });
    };

    presenter.setState = function (state) {
        if (!state) return;

    	var parsedState = JSON.parse(state),
            selectedItem = parsedState.selectedItem,
            currentRate = parsedState.currentRate;
            
    		
        presenter.setCommentValue(parsedState.commentValue);
        presenter.setSelectedImage(selectedItem);
        presenter.currentRate = currentRate ? currentRate : "0";
    	presenter.configuration.isVisible = parsedState.isVisible;
    	presenter.setVisibility(presenter.configuration.isVisible);
    };
    
    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'getRate': presenter.getRate
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };
    
    presenter.getRate = function() {
		return presenter.currentRate;
    };

    presenter.reset = function () {
        presenter.isElementSelected = null;
        var $img = presenter.$view.find("img[name='selected']"),
            index = parseInt($img.data('index'), 10);

        $img.attr({
            "src": deselected_img[index],
            "name": "deselected"
        });

        presenter.$view.find('textarea[data-name="textarea"]').val("");
        presenter.$view.find('.page-rating-submit-button').attr('disabled','disabled');
    };
        
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Page_Score_Counter" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Display Mode" name="DisplayMode" nameLabel="Page_Score_Counter_property_display_mode" type="{Fraction (Score/Max Score),Score,Max Score}"/>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
    </model>
<css>.addon_Page_Score_Counter {
    color: green;
}

.page-score-counter-wrapper &gt; div.hidden {
    display: none;
}

.page-score-counter-wrapper &gt; div {
    text-align: center;
}

.printable_page-score-counter-wrapper &gt; div {
    text-align: center;
}

.page-score-counter-wrapper .fraction &gt; div {
    text-align: left;
    display: inline-block;
}

.printable_page-score-counter-wrapper .printable_fraction &gt; div {
    text-align: left;
    display: inline-block;
}
</css><view>&lt;div class="page-score-counter-wrapper"&gt;
    &lt;div class="fraction"&gt;
        &lt;div class="score"&gt;
        &lt;/div&gt;
        &lt;div class="separator"&gt;
            /
        &lt;/div&gt;
        &lt;div class="max-score"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="score hidden"&gt;
    &lt;/div&gt;
    &lt;div class="max-score hidden"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="page-score-counter-wrapper"&gt;
    &lt;div class="fraction hidden"&gt;
        &lt;div class="score"&gt;
            6
        &lt;/div&gt;
        &lt;div class="separator"&gt;
            /
        &lt;/div&gt;
        &lt;div class="max-score"&gt;
            13
        &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="score hidden"&gt;
        6
    &lt;/div&gt;
    &lt;div class="max-score hidden"&gt;
        13
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonPage_Score_Counter_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.printableController = null;
    presenter.eventBus = null;
    presenter.isVisible = true;
    presenter.isScoreVisible = false;
    presenter.currentScore = 0;
    presenter.maxScore = 0;
    presenter.printableState = null;
    presenter.printableStateMode = null;

    presenter.DISPLAY_MODE = {
        FRACTION: 1,
        SCORE: 2,
        MAX_SCORE: 3
    };

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_RESULTS: 1
    };

    presenter.CSS_CLASSES = {
        PRINTABLE_ADDON: "printable_addon_Page_Score_Counter",
        PRINTABLE_WRAPPER: "printable_page-score-counter-wrapper",
        PRINTABLE_FRACTION: "printable_fraction",
        PRINTABLE_SCORE: "printable_score",
        PRINTABLE_SEPARATOR: "printable_separator",
        PRINTABLE_MAX_SCORE: "printable_max-score"
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();

        var presentation = presenter.playerController.getPresentation();
        presenter.page = presentation.getPage(presenter.playerController.getCurrentPageIndex());
    };

    presenter.setPrintableController = function (controller) {
        presenter.printableController = controller;
    };

    presenter.createEventData = function (score) {
        return {
            'source': presenter.configuration.addonID,
            'item': 'pageScore',
            'value' : '',
            'score': score
        };
    };

    presenter.sendEvent = function(eventName, eventData) {
        presenter.eventBus.sendEvent(eventName, eventData);
    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        return {
            'isVisible' : ModelValidationUtils.validateBoolean(model["Is Visible"]),
            'addonID' : model['ID'],
            'displayMode' : getDisplayMode(model['DisplayMode'])
        }
    };

    function getDisplayMode(rawDisplayMode) {
        if (rawDisplayMode == 'Fraction (Score/Max Score)' || rawDisplayMode == '') { // when dropdown hasn't been changed it gives you empty string
            return presenter.DISPLAY_MODE.FRACTION;
        } else if (rawDisplayMode == 'Score'){
            return presenter.DISPLAY_MODE.SCORE;
        } else if (rawDisplayMode == 'Max Score') {
            return presenter.DISPLAY_MODE.MAX_SCORE;
        }
    }

    presenter.attachEventHandler = function () {
        if (presenter.page.isReportable()) {
            presenter.eventBus.addEventListener('ValueChanged', this);
            presenter.eventBus.addEventListener('PageLoaded', this);
        }
    };

    function runLogic(view, model, isPreview) {
        presenter.$view = $(view);
        presenter.configuration = presenter.validateModel(model);
        presenter.isPreview = isPreview;

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        presenter.$fractionWrapper = presenter.$view.find('.page-score-counter-wrapper &gt; .fraction');
        presenter.$scoreWrapper = presenter.$view.find('.page-score-counter-wrapper &gt; .score');
        presenter.$maxScoreWrapper = presenter.$view.find('.page-score-counter-wrapper &gt; .max-score');

        if (!isPreview) {
            presenter.attachEventHandler();
        } else {
            toggleBoxesVisibility();
        }
    }

    function toggleBoxesVisibility() {
        if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.FRACTION) {
            toggleBoxVisibility(presenter.$fractionWrapper);
        } else if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.SCORE) {
            toggleBoxVisibility(presenter.$scoreWrapper);
        } else if (presenter.configuration.displayMode == presenter.DISPLAY_MODE.MAX_SCORE) {
            toggleBoxVisibility(presenter.$maxScoreWrapper);
        }
    }

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName == "ValueChanged" &amp;&amp; eventData.source != presenter.configuration.addonID) {
            if(eventData.item != 'pageScore'){
                presenter.countScore();
                updateView();
                presenter.sendEvent('ValueChanged', presenter.createEventData(presenter.currentScore));
            }
        }
        if (eventName == 'PageLoaded') {
            presenter.countScore();
            updateView();
        }
    };

    presenter.countScore = function () {
        var scoreService = presenter.playerController.getScore(),
            pageScore = scoreService.getPageScoreById(presenter.page.getId());

        presenter.currentScore = pageScore.score;
        presenter.maxScore = pageScore.maxScore;
    };

    function updateView() {
        if (!presenter.isScoreVisible) {
            toggleBoxesVisibility();
            presenter.isScoreVisible = true;
        }

        presenter.$fractionWrapper.find('.score').html(presenter.currentScore);
        presenter.$fractionWrapper.find('.max-score').html(presenter.maxScore);
        presenter.$scoreWrapper.html(presenter.currentScore);
        presenter.$maxScoreWrapper.html(presenter.maxScore);
    }

    function toggleBoxVisibility($element) {
        presenter.$view.find('.page-score-counter-wrapper &gt; div').addClass('hidden');
        $element.removeClass('hidden');
    }

    presenter.run = function(view, model) {
       runLogic(view, model, false);

        if(presenter.configuration.displayMode == presenter.DISPLAY_MODE.FRACTION){
            presenter.$fractionWrapper.find('.score').text('0');
            presenter.$fractionWrapper.find('.max-score').html('0');
        }
        if(presenter.configuration.displayMode == presenter.DISPLAY_MODE.SCORE){
            presenter.$scoreWrapper.text('0');
            presenter.$scoreWrapper.removeClass('hidden');
            presenter.$view.find('.separator').css('display', 'none');
        }
        if(presenter.configuration.displayMode == presenter.DISPLAY_MODE.MAX_SCORE){
            presenter.$maxScoreWrapper.text('0');
            presenter.$maxScoreWrapper.removeClass('hidden');
            presenter.$view.find('.separator').css('display', 'none');
        }
    };

    presenter.reset = function() {
        presenter.setVisibility(presenter.configuration.isVisible);
        var scoreService = presenter.playerController.getScore();
        presenter.currentScore = scoreService.getPageScoreWithOnlyActiveOpenActivitiesById(presenter.page.getId());
        updateView();
    };

    presenter.setVisibility = function (isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'getPageScore' : presenter.getPageScore,
            'getPageMaxScore' : presenter.getPageMaxScore
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getPageScore = function() {
        return presenter.currentScore;
    };

    presenter.getPageMaxScore = function() {
        return presenter.maxScore;
    };

    presenter.getState = function() {
        return JSON.stringify({
            'isVisible' : presenter.isVisible,
            'isScoreVisible': presenter.isScoreVisible,
            'score' : presenter.currentScore,
            'maxScore' : presenter.maxScore
        });
    };

    presenter.setState = function(state) {
        if (!state) {
            return;
        }

        var parsedState = JSON.parse(state);

        presenter.currentScore = parsedState.score;
        presenter.maxScore = parsedState.maxScore;

        if (parsedState.isScoreVisible) {
            updateView();
        }

        presenter.setVisibility(parsedState.isVisible);
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        chosePrintableStateMode(showAnswers);
        const configuration = presenter.validateModel(model);

        const view = createPrintableBaseView(model);
        const wrapper = createPrintableWrapper();
        view.append(wrapper);

        switch (configuration.displayMode) {
            case presenter.DISPLAY_MODE.FRACTION:
                wrapper.append(createPrintableFraction());
                break;
            case presenter.DISPLAY_MODE.SCORE:
                wrapper.append(createPrintableScore());
                break;
            case presenter.DISPLAY_MODE.MAX_SCORE:
                wrapper.append(createPrintableMaxScore());
                break;
        }

        presenter.printableStateMode = null;
        return view.outerHTML;
    };

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState &amp;&amp; showAnswers) {
            presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_RESULTS;
        } else {
            presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function isInPrintableShowResultsStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_RESULTS;
    }

    function createPrintableBaseView(model) {
        const element = document.createElement("div");
        element.id = model.ID;
        element.classList.add(presenter.CSS_CLASSES.PRINTABLE_ADDON);
        return element;
    }

    function createPrintableWrapper() {
        const wrapper = document.createElement("div");
        wrapper.classList.add(presenter.CSS_CLASSES.PRINTABLE_WRAPPER);
        return wrapper;
    }

    function createPrintableFraction() {
        const element = document.createElement("div");
        element.classList.add(presenter.CSS_CLASSES.PRINTABLE_FRACTION);
        element.append(createPrintableScore());
        element.append(createPrintableSeparator());
        element.append(createPrintableMaxScore());
        return element;
    }

    function createPrintableScore() {
        const element = document.createElement("div");
        element.classList.add(presenter.CSS_CLASSES.PRINTABLE_SCORE);
        element.innerHTML = isInPrintableShowResultsStateMode()
            ? "" + presenter.printableState.score
            : "&amp;nbsp;";
        return element;
    }

    function createPrintableSeparator() {
        const element = document.createElement("div");
        element.classList.add(presenter.CSS_CLASSES.PRINTABLE_SEPARATOR);
        element.innerText = "/";
        return element;
    }

    function createPrintableMaxScore() {
        const element = document.createElement("div");
        element.classList.add(presenter.CSS_CLASSES.PRINTABLE_MAX_SCORE);
        element.innerText = presenter.printableController.getPageMaxScore();
        return element;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Paragraph" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Default font family" nameLabel="Paragraph_property_default_font_family" type="string"/>
		<property name="Default font size" nameLabel="Paragraph_property_default_font_size" type="string"/>
		<property name="Hide toolbar" nameLabel="Paragraph_property_hide_toolbar" type="boolean"/>
		<property name="Editable placeholder" nameLabel="Paragraph_property_editable_placeholder" type="boolean"/>
		<property name="Custom toolbar" nameLabel="Paragraph_property_custom_toolbar" type="string"/>
		<property name="Custom CSS" nameLabel="Paragraph_property_custom_css" type="file"/>
		<property name="useCustomCSSFiles" nameLabel="Paragraph_property_use_custom_css_files" type="boolean"/>
		<property name="Placeholder Text" nameLabel="Paragraph_property_placeholder_text" type="html"/>
		<property name="Layout Type" nameLabel="Paragraph_property_layout_type" type="{Default, French}"/>
		<property name="Manual grading" nameLabel="Paragraph_property_manual_grading" type="boolean"/>
		<property name="Show Answers" nameLabel="Paragraph_property_show_answers" type="list">
			<property displayName="Text" name="Text" nameLabel="Paragraph_property_show_answers_field_text" type="html"/>
		</property>
		<property name="Title" nameLabel="Paragraph_property_title" type="string"/>
		<property name="Weight" nameLabel="Paragraph_property_weight" type="string"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="Paragraph_property_speech_texts" type="staticlist">
            <property name="Selected" nameLabel="Paragraph_property_selected" type="staticrow">
                <property name="Selected" nameLabel="Paragraph_property_selected" type="string"/>
            </property>
            <property name="Deselected" nameLabel="Paragraph_property_deselected" type="staticrow">
                <property name="Deselected" nameLabel="Paragraph_property_deselected" type="string"/>
            </property>
			<property name="ParagraphContent" nameLabel="Paragraph_property_ParagraphContent" type="staticrow">
				<property name="ParagraphContent" nameLabel="Paragraph_property_ParagraphContent" type="string"/>
			</property>
			<property name="Bold" nameLabel="Paragraph_property_bold" type="staticrow">
				<property name="Bold" nameLabel="Paragraph_property_bold" type="string"/>
			</property>
			<property name="Italic" nameLabel="Paragraph_property_italic" type="staticrow">
				<property name="Italic" nameLabel="Paragraph_property_italic" type="string"/>
			</property>
			<property name="Underline" nameLabel="Paragraph_property_underline" type="staticrow">
				<property name="Underline" nameLabel="Paragraph_property_underline" type="string"/>
			</property>
			<property name="AlignLeft" nameLabel="Paragraph_property_AlignLeft" type="staticrow">
				<property name="AlignLeft" nameLabel="Paragraph_property_AlignLeft" type="string"/>
			</property>
			<property name="AlignCenter" nameLabel="Paragraph_property_AlignCenter" type="staticrow">
				<property name="AlignCenter" nameLabel="Paragraph_property_AlignCenter" type="string"/>
			</property>
			<property name="AlignRight" nameLabel="Paragraph_property_AlignRight" type="staticrow">
				<property name="AlignRight" nameLabel="Paragraph_property_AlignRight" type="string"/>
			</property>
			<property name="Justify" nameLabel="Paragraph_property_Justify" type="staticrow">
				<property name="Justify" nameLabel="Paragraph_property_Justify" type="string"/>
			</property>
		</property>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Paragraph_property_lang_attribute" type="string"/>
		<property name="Block in error checking mode" nameLabel="Paragraph_property_block_in_error_checking_mode" type="boolean"/>
	</model>
<css>div.paragraph-wrapper {
	width: 100%;
	height: 100%;
	border: solid 1px #9E9E9E;
	border-radius: 2px;
    overflow-y: hidden;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

div.paragraph-wrapper.disabled {
	pointer-events: none;
  	cursor: default;
	opacity: 0.5;
}

div.paragraph-wrapper textarea {
	width: 100%;
	height: 100%;
}

div.paragraph-lock {
	width: 100%;
	height: 100%;
	background: #e8eef3;
	position: absolute;
	top: 1px;
}

.keyboard-navigation-margin {
	margin: 5px !important;
}
</css><view>&lt;div class="paragraph-wrapper"&gt;
	&lt;form&gt;
		&lt;textarea class="paragraph_field"&gt;&lt;/textarea&gt;
	&lt;/form&gt;
&lt;/div&gt;
</view><preview>&lt;div class="paragraph-wrapper"&gt;
	&lt;form&gt;
		&lt;textarea class="paragraph_field"&gt;&lt;/textarea&gt;
	&lt;/form&gt;
&lt;/div&gt;
</preview><presenter>function AddonParagraph_create() {
    var presenter = function () {};
    var eventBus;
    var userAnswerAIReviewRequest = 'gradeByAi';
    var userAnswerAIReviewResponse = 'aiGraded';

    presenter.placeholder = null;
    presenter.editor = null;
    presenter.jQueryTinyMCEHTML = null;
    presenter.$tinyMCEToolbar = null;
    presenter.tinyMceContainer = null;
    presenter.editor = null;
    presenter.playerController = null;
    presenter.isVisibleValue = null;
    presenter.isShowAnswersActive = false;
    presenter.wasShowAnswersActive = false;
    presenter.isErrorCheckingMode = false;
    presenter.cachedAnswer = [];
    presenter.currentGSAIndex = 0;
    presenter.savedInitializedSA = null;

    presenter.isEditorLoaded = false;
    presenter.isEditorReadOnly = false;

    presenter.toolbarChangeHeightTimeoutID = null;
    presenter.paragraphInitTimeoutID = null;
    presenter.currentPageIndex = null;
    presenter.isWaitingForAIGrade = false;
    presenter.aiGradeInterval = null;
    presenter.MAX_WAIT_TIME = 15;
    presenter.startTime = null;
    presenter.updateScoreEventName = 'updateScore';
    presenter.printableState = null;

    presenter.LANGUAGES = {
        DEFAULT: "en_GB",
        FRENCH: "fr_FR",
        FRENCH_PURE: "fr_FR_pure"
    };

    presenter.DEFAULTS = {
        TOOLBAR: 'bold italic underline numlist bullist alignleft aligncenter alignright alignjustify',
        FONT_FAMILY: 'Verdana,Arial,Helvetica,sans-serif',
        FONT_SIZE: '11px',
        BUTTON_WIDTH: 37,
        FORMAT_WIDTH: 85,
        STYLE_SELECT_NAME: "styleselect"
    };

    presenter.ALLOWED_TOOLBAR_BUTTONS = 'customBold customUnderline customItalic newdocument bold italic underline strikethrough alignleft aligncenter '+
        'alignright alignjustify styleselect formatselect fontselect fontsizeselect '+
        'bullist numlist outdent indent blockquote undo redo '+
        'removeformat subscript superscript forecolor backcolor |'.split(' ');

    presenter.ERROR_CODES = {
        'W_01': 'Weight must be a whole number between 0 and 100',
        'W_02': 'Occurs a problem with checking your answers. Please try later.'
    };

    presenter.TOOLBAR_ARIAS = {
        bold: "bold",
        italic: "italic",
        underline: "underline",
        alignleft: "alignLeft",
        aligncenter: "alignCenter",
        alignright: "alignRight",
        justify: "justify"
    };

    presenter.DEFAULT_TTS_PHRASES = {
        selected: "selected",
        deselected:"deselected",
        paragraphContent: "paragraph content",
        bold: "bold",
        italic: "italic",
        underline: "underline",
        alignLeft: "align left",
        alignCenter: "align center",
        alignRight: "align right",
        justify: "justify"
    };

    presenter.keys = {
        ESCAPE: 27
    };

    function isIOSSafari() {
        var ua = window.navigator.userAgent,
            iOS = !!ua.match(/iPad/i) || !!ua.match(/iPhone/i),
            webkit = !!ua.match(/WebKit/i),
            iOSSafari = iOS &amp;&amp; webkit &amp;&amp; !ua.match(/CriOS/i);
        return iOSSafari
    }

    presenter.executeCommand = function AddonParagraph_executeCommand(name, params) {
        if (!presenter.configuration.isValid) { return; }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isVisible': presenter.isVisible,
            'getText': presenter.getText,
            'setText': presenter.setText,
            'isAttempted': presenter.isAttempted,
            'lock': presenter.lock,
            'unlock': presenter.unlock,
            'isAIReady': presenter.isAIReady,
            'sendUserAnswerToAICheck': presenter.sendUserAnswerToAICheck,
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.isAttempted = function () {
        if (!presenter.isEditorLoaded) {
            if (presenter.state) {
                var parser = new DOMParser();
                var stateNode = parser.parseFromString(JSON.parse(presenter.state).tinymceState, "text/html");
                return $(stateNode).text() != '';
            } else {
                return false;
            }
        }

        const editorContent = presenter.getText();
        if (presenter.configuration.isPlaceholderSet
            &amp;&amp; !presenter.configuration.isPlaceholderEditable) {
            return !isPlaceholderClassInHTML(editorContent);
        }
        const textToCompare = presenter.configuration.isPlaceholderSet ? presenter.configuration.placeholderText : "";
        return $(editorContent).text() != textToCompare;
    };

    presenter.getText = function AddonParagraph_getText() {
        return presenter.editor.getContent({format: 'raw'});
    };

    presenter.sendOnBlurEvent = function () {
        var eventData = {
            'source': presenter.configuration.ID,
            'item': '',
            'value': 'blur',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.removeFocusFromDisabledElement = function () {
        if (presenter.isEditorReadOnly) {
            const iframe = presenter.$view.find('iframe');
            iframe.blur();
        }
    };

    presenter.setVisibility = function AddonParagraph_setVisibility(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        if (isVisible) {
            presenter.$view.find(".paragraph-wrapper").show();
        } else {
            presenter.$view.find(".paragraph-wrapper").hide();
        }

        presenter.isVisibleValue = isVisible;
    };

    presenter.createPreview = function AddonParagraph_createPreview(view, model) {
        presenter.initializeEditor(view, model, true);
        presenter.setVisibility(true);
        var clickhandler = $("&lt;div&gt;&lt;/div&gt;").css({"background":"transparent", 'width': '100%', 'height': '100%', 'position':'absolute', 'top':0, 'left':0});
        presenter.$view.append(clickhandler);
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;
        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers', 'ValueChanged'];
        for (var i = 0; i &lt; events.length; i++) {
            eventBus.addEventListener(events[i], this);
        }
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;

            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "HideAnswers":
            case "GradualHideAnswers":
                presenter.hideAnswers();
                break;

            case "ValueChanged":
                presenter.handleUpdateScoreEvent(eventData);
                break;
        }
    };

    presenter.enableEdit = function () {
        if(presenter.isEditorReadOnly) {
            presenter.editor.setMode('design');
            presenter.isEditorReadOnly = false;
        }
    };

    presenter.disableEdit = function () {
        if(!presenter.isEditorReadOnly) {
            presenter.editor.setMode('readonly');
            presenter.isEditorReadOnly = true;
        }
    };

    presenter.showAnswers = function () {
        if (presenter.isShowAnswersActive) return;

        setTimeout(function () {
            const elements = presenter.getParagraphs();
            presenter.initializeShowAnswers(elements);
            presenter.savedInitializedSA = presenter.getText();
            var modelAnswer = combineAnswers(presenter.configuration.modelAnswer);
            presenter.editor.setContent(modelAnswer);
            presenter.setStyles();
            presenter.isShowAnswersActive = true;
            presenter.wasShowAnswersActive = true;
            presenter.isErrorCheckingMode = false;
        }, 0);
    };

    presenter.initializeShowAnswers = function Addon_Paragraph_initializeShowAnswers (elements) {
        presenter.disableEdit();
        presenter.cacheAnswersAndClearParagraphs(elements);
    };

    presenter.cacheAnswersAndClearParagraphs = function (elements) {
        $(elements).each((index, element) =&gt; {
            presenter.cachedAnswer.push(element.innerHTML);
            element.innerHTML = "";
        })
    };

    function combineAnswers(answersArray) {
        var newText = "";
        if (answersArray.length &gt; 0) {
           newText += answersArray[0].Text;
        }
        for (var answerID = 1; answerID &lt; answersArray.length; answerID++) {
           newText += "&lt;div&gt;&lt;/div&gt;&lt;br&gt;" + answersArray[answerID].Text;
        }
        return newText;
    }

    presenter.hideAnswers = function (shouldEnableEdit = true) {
        if (presenter.savedInitializedSA) {
            presenter.setText(presenter.savedInitializedSA);
            presenter.savedInitializedSA = null;
        }
        const elements = presenter.getParagraphs();

        if (shouldEnableEdit) {
            presenter.enableEdit();
        }
        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
        presenter.isErrorCheckingMode = false;
        presenter.currentGSAIndex = 0;

        if (presenter.cachedAnswer.length) {
            for (var [key, value] of Object.entries(elements)) {
                if (+key &gt; -1) {
                    value.innerHTML = presenter.cachedAnswer[+key];
                }
            }
            presenter.cachedAnswer = [];
        }
    };

    presenter.handleDisablingEdition = function () {
        if (presenter.wasShowAnswersActive &amp;&amp; presenter.currentGSAIndex === 0) {
            presenter.wasShowAnswersActive = false;
            presenter.hideAnswers(false);
            setTimeout(function () {
                presenter.disableEdit();
            }, 0);
        } else {
            presenter.disableEdit();
        }
    };

    presenter.gradualShowAnswers = function (data) {
        presenter.handleDisablingEdition();

        if (data.moduleID !== presenter.configuration.ID) { return; }

        setTimeout(function () {
            const elements = presenter.getParagraphs();
            if (!presenter.isGradualShowAnswersActive) {
                presenter.initializeShowAnswers(elements);
                presenter.isGradualShowAnswersActive = true;
            }
            presenter.isErrorCheckingMode = false;

            if (presenter.currentGSAIndex !== 0) {
                elements[0].innerHTML += "&lt;div&gt;&lt;/div&gt;&lt;br&gt;";
            }
            elements[0].innerHTML += presenter.configuration.modelAnswer[presenter.currentGSAIndex].Text;
            presenter.currentGSAIndex++;
        }, 0);
    };

    presenter.sendUserAnswerToAICheck = function () {
        if(!presenter.isAIReady() || presenter.isWaitingForAIGrade) { return; }

        const data = presenter.getDataRequestToAI();

        window.addEventListener("message", presenter.onExternalMessage);
        presenter.playerController.sendExternalEvent(userAnswerAIReviewRequest, data);
        presenter.isWaitingForAIGrade = true;
        presenter.waitForAIGrade();
    };

    presenter.getDataRequestToAI = function () {
        const pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
        const activityID = presenter.configuration.ID;
        const activityMaxScore = presenter.getMaxScore();
        const pageWeight = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getPageWeight();
        const pageMaxScore = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getModulesMaxScore();
        const answer = presenter.getText().replace(/&lt;(.*?)&gt;/g, '').replace(/&amp;nbsp;/g, '');

        return JSON.stringify({
            'page_id': pageID,
            'activity_id': activityID,
            'activity_max_score': activityMaxScore,
            'page_weight': pageWeight,
            'page_max_score': pageMaxScore,
            'answer': answer
        });
    };

    presenter.onExternalMessage = function (event) {
        const data = event.data;

        if (presenter.isValidResponse(data)) {
            presenter.updateOpenActivityScore(data);
        }
    };

    presenter.isValidResponse = function (data) {
        const isAIResponse = data.includes(userAnswerAIReviewResponse);
        if (!isAIResponse) { return isAIResponse; }

        const pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
        const activityID = presenter.configuration.ID;
        const isValidPageID = data.includes(pageID);
        const isValidActivityID = data.includes(activityID);

        return isAIResponse &amp;&amp; isValidPageID &amp;&amp; isValidActivityID;
    };

    presenter.updateOpenActivityScore = function (data) {
        const parsedData = JSON.parse(data.replace(`EXTERNAL_${userAnswerAIReviewResponse}:`, '').trim());
        const pageID = parsedData.page_id;
        const activityID = parsedData.activity_id;
        const aiGrade = parsedData.ai_grade;
        const aiRelevance = parsedData.ai_relevance;

        OpenActivitiesUtils.updateOpenActivityScore(
            presenter.playerController,
            pageID,
            activityID,
            aiGrade,
            aiRelevance
        );
    };

    presenter.setShowErrorsMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (!presenter.isErrorCheckingMode &amp;&amp; presenter.configuration.isBlockedInErrorCheckingMode) {
            presenter.isErrorCheckingMode = true;
            presenter.disableEdit();
        }
    };

    presenter.setWorkMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (presenter.isErrorCheckingMode &amp;&amp; presenter.configuration.isBlockedInErrorCheckingMode) {
            presenter.isErrorCheckingMode = false;
            presenter.enableEdit();
        }
    };

    presenter.getParagraphs = function () {
        const $paragraph = presenter.$view.find(".paragraph-wrapper"),
            $iframe = $paragraph.find("iframe"),
            $iframeBody = $iframe.contents().find("#tinymce");

        return $iframeBody.children();
    };

    presenter.run = function AddonParagraph_run(view, model) {
        presenter.initializeEditor(view, model, false);
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.isLocked = false;
    };

    presenter.initializeEditor = function AddonParagraph_initializeEditor(view, model, isPreview) {
        if (presenter.loaded){ return;}
        presenter.loaded = true;
        presenter.view = view;
        presenter.$view = $(view);
        presenter.isPreview = isPreview;
        var upgradedModel = presenter.upgradeModel(model);
        presenter.model = upgradedModel;
        presenter.configuration = presenter.validateModel(upgradedModel, isPreview);

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.$view.on('click', function viewClickHandler(e) {
            e.stopPropagation();
            e.preventDefault();
        });

        presenter.setWrapperID();

        if (presenter.playerController) {
            var paragraphInitDelay = presenter.playerController.getExternalVariable('paragraphInitDelay');
            if (paragraphInitDelay.length == 0 || isNaN(paragraphInitDelay)) {
                paragraphInitDelay = 10;
            }
            presenter.$view.css("visibility", "hidden");
            presenter.paragraphInitTimeoutID = setTimeout(presenter.initTinymce, Number(paragraphInitDelay));
        } else {
            presenter.initTinymce();
        }
    };

    function sendPreDestroyedEmptyEvent() {
        presenter.sendEmptyEvent(false);
    }

    function sendValueChangedEmptyEvent() {
        presenter.sendEmptyEvent(true);
    }

    presenter.sendEmptyEvent = function (isValueChangedEvent) {
        const eventName = isValueChangedEvent ? 'ValueChanged' : 'PreDestroyed';
        const eventData = {
            'source': presenter.configuration.ID,
            'item': '',
            'value': 'empty',
        };
        if (isValueChangedEvent) {
            eventData.score = '';
        }

        eventBus.sendEvent(eventName, eventData);
    };

    presenter.sendModifiedEvent = function () {
        const eventData = {
            'source': presenter.configuration.ID,
            'item': '',
            'value': 'modified',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.initTinymce = function () {
        if (!document.body.contains(presenter.view)) {
            return;
        }
        if (presenter.isPreview || !presenter.configuration.useCustomCSSFiles || !presenter.configuration.content_css) {
            presenter.initTinymceWithoutParsingContentCSS();
        } else {
            presenter.initTinymceWithParsingContentCSS();
        }
    };

    presenter.initTinymceWithoutParsingContentCSS = function () {
        _initTinymce(presenter.configuration.content_css);
    };

    presenter.initTinymceWithParsingContentCSS = function () {
        presenter.$view.css("visibility", "hidden");
        URLUtils.parseCSSFile(presenter.playerController, presenter.configuration.content_css)
            .then((newCSSText) =&gt; {
                const newCSS = new File(
                    [newCSSText],
                    `parsed ${presenter.configuration.content_css}.css`,
                    {type: "text/css"}
                );
                return URL.createObjectURL(newCSS);
            })
            .then((contentCSSURL) =&gt; {
                _initTinymce(contentCSSURL);
            }).catch(() =&gt; {
                console.warn(`Failed to download assets provided in "Custom CSS" file for Paragraph ${presenter.configuration.ID} addon`);
            });
    };

    function _initTinymce(contentCSSURL) {
        presenter.placeholder = new presenter.placeholderElement();
        presenter.configuration.plugins = presenter.getPlugins();
        presenter.addPlugins();

        tinymce.init(presenter.getTinymceInitConfiguration(presenter.getTinyMceSelector(), contentCSSURL)).then(function (editors) {
            presenter.editor = editors[0];
            presenter.onInit();

            if (isIOSSafari()) {
                presenter.findIframeAndSetStyles();
            }

            presenter.editor.on('blur', presenter.onEditorBlur);

            presenter.editor.on('focus', function () {
                presenter.removeFocusFromDisabledElement();
            });

            presenter.isEditorLoaded = true;
            presenter.setStyles();
            presenter.setSpeechTexts(presenter.model["speechTexts"]);
            presenter.buildKeyboardController();
        });

        if(isIOSSafari()) {
            var input = document.createElement("input");
            input.type = "text";
            $(input).css('display', 'none');
            presenter.$view.append(input);
        }
        presenter.paragraphInitTimeoutID = null;
    }

    presenter.onEditorBlur = function() {
        presenter.sendOnBlurEvent();
        if (presenter.isAttempted()) {
            presenter.sendModifiedEvent();
        } else {
            sendValueChangedEmptyEvent();
        }
    };

    presenter.setWrapperID = function AddonParagraph_setWrapperID() {
        var $paragraphWrapper = presenter.$view.find('.paragraph-wrapper');
        $paragraphWrapper.attr('id', presenter.configuration.ID + '-wrapper');
    };

    presenter.getTinyMceSelector = function AddonParagraph_getTinyMceSelector() {
        return '#' + presenter.configuration.ID + '-wrapper .paragraph_field';
    };

    presenter.getTinymceInitConfiguration = function AddonParagraph_getTinyMceConfiguration(selector, contentCSSURL) {
        var layoutType = presenter.configuration.layoutType;

        var language = layoutType === "Default"
            ? presenter.LANGUAGES.DEFAULT
            : layoutType === "French_pure"
                ? presenter.LANGUAGES.FRENCH_PURE
                : presenter.LANGUAGES.FRENCH;

        var toolbar = layoutType === "Default"
            ? presenter.configuration.toolbar
            : layoutType === "French"
                ? presenter.getSpecifyToolbar(layoutType)
                : presenter.configuration.toolbar;

        return {
            plugins: presenter.configuration.plugins,
            selector : selector,
            width: presenter.configuration.width,
            height: presenter.configuration.textAreaHeight,
            statusbar: false,
            menubar: false,
            toolbar: toolbar,
            content_css: contentCSSURL,
            setup: presenter.setup,
            language: language
        };
    };

    presenter.findIframeAndSetStyles = function AddonParagraph_findIframeAndSetStyles() {
        var iframe = presenter.$view.find(".paragraph-wrapper").find("iframe"),
            body = $(iframe).contents().find("#tinymce"),
            element = body.find("p");

        element.css({
            'overflow-wrap': 'break-word',
            'word-wrap': 'break-word',
            '-ms-word-break': 'break-all',
            'word-break': 'break-word',
            '-ms-hyphen': 'auto',
            '-moz-hyphens': 'auto',
            '-webkit-hyphens': 'auto',
            'hyphens': 'auto'
        });

        body.css('min-height', 'initial');

         if(presenter.configuration.isToolbarHidden) {
             iframe.css('height', presenter.configuration.height);
         }

        presenter.$view.find(".paragraph-wrapper").css("overflow", "scroll");
    };

    presenter.validateToolbar = function AddonParagraph_validateToolbar(controls, width) {
        if (!controls) {
            controls = presenter.DEFAULTS.TOOLBAR;
        }

        controls = controls.split(" ");
        if (controls.indexOf("|") != -1) {
            return presenter.parseToolbarWithGroups(controls, width);
        } else {
            return presenter.parseToolbarWithoutGroups(controls, width);
        }
    };

    presenter.parseToolbarWithGroups = function (controls, toolbarWidth) {
        var controlGroups = controls.join(" ").split("|");
        return controlGroups.filter(function (group) {
            return group.trim().length &gt; 0;
        }).map(function (group) {
            return presenter.parseToolbarWithoutGroups(group.trim().split(" "), toolbarWidth);
        }).join(" | ");
    };

    presenter.parseToolbarWithoutGroups = function (controls, toolbarWidth) {
        var filteredControls = controls.filter(function(param){
            return presenter.ALLOWED_TOOLBAR_BUTTONS.indexOf(param) != -1;
        });

        var result = "";
        var bufor = 0;
        var widthToAdd = 0;
        for(var i = 0; i &lt; filteredControls.length; i++) {
            if (filteredControls[i] !== presenter.DEFAULTS.STYLE_SELECT_NAME) {
                widthToAdd = presenter.DEFAULTS.BUTTON_WIDTH;
            } else {
                widthToAdd = presenter.DEFAULTS.FORMAT_WIDTH;
            }

            if (bufor + widthToAdd &lt; toolbarWidth) {
                bufor += widthToAdd;
                result += filteredControls[i].trim() + " ";
            } else {
                bufor = widthToAdd;
                result += "| " + filteredControls[i].trim() + " ";
            }
        }

        return result.trim();
    };

    /**
     * Parses model and set settings to default values if either of them is empty
     *
     * IMPORTANT. Validation resulting in a validation error, e.g. W_01, must be implemented in Open Activities
     * on the mCourser side.
     *
     * @param model:object
     * @param isPreview:boolean
     * @returns {{fontFamily: *, fontSize: *}}
     */
    presenter.validateModel = function AddonParagraph_validateModel(model, isPreview) {
        var fontFamily = model['Default font family'],
            fontSize = model['Default font size'],
            isToolbarHidden = ModelValidationUtils.validateBoolean(model['Hide toolbar']),
            isPlaceholderEditable = ModelValidationUtils.validateBoolean(model['Editable placeholder']),
            toolbar = presenter.validateToolbar(model['Custom toolbar'], model.Width),
            height = model.Height,
            hasDefaultFontFamily = false,
            hasDefaultFontSize = false,
            layoutType = model["Layout Type"] || "Default",
            title = model["Title"],
            manualGrading = ModelValidationUtils.validateBoolean(model["Manual grading"]),
            weight = model['Weight'],
            modelAnswer = model['Show Answers'];

        if (ModelValidationUtils.isStringEmpty(fontFamily)) {
            fontFamily = presenter.DEFAULTS.FONT_FAMILY;
            hasDefaultFontFamily = true;
        }

        if (ModelValidationUtils.isStringEmpty(fontSize)) {
            fontSize = presenter.DEFAULTS.FONT_SIZE;
            hasDefaultFontSize = true;
        }

        const validatedWeight = presenter.validateWeight(weight, isPreview);
        if (!validatedWeight.isValid) {
            return validatedWeight;
        }

        height -= !isToolbarHidden ? 37 : 2;

        return {
            ID: model["ID"],
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isValid: true,
            fontFamily: fontFamily,
            fontSize: fontSize,
            isToolbarHidden: isToolbarHidden,
            toolbar: toolbar,
            textAreaHeight: height,
            hasDefaultFontFamily: hasDefaultFontFamily,
            hasDefaultFontSize: hasDefaultFontSize,
            content_css: model['Custom CSS'],
            useCustomCSSFiles: ModelValidationUtils.validateBoolean(model["useCustomCSSFiles"]),
            isPlaceholderSet: !ModelValidationUtils.isStringEmpty(model["Placeholder Text"]),
            placeholderText: model["Placeholder Text"],
            pluginName: presenter.makePluginName(model["ID"]),
            width: model['Width'],
            height: parseInt(height, 10),
            layoutType: layoutType,
            isPlaceholderEditable: isPlaceholderEditable,
            title: title,
            manualGrading: manualGrading,
            weight: validatedWeight.value,
            modelAnswer: modelAnswer,
            langTag: model["langAttribute"],
            isBlockedInErrorCheckingMode: ModelValidationUtils.validateBoolean(model["Block in error checking mode"]),
        };
    };

    presenter.validateWeight = function (weight, isPreview) {
        if (ModelValidationUtils.isStringEmpty(weight)) {
            return getCorrectObject(1);
        }

        const validatedInteger = ModelValidationUtils.validateIntegerInRange(weight, 100, 0);
        if (!validatedInteger.isValid) {
            return ModelErrorUtils.getErrorObject("W_01");
        }
        if (isPreview &amp;&amp; (validatedInteger.value + "") !== weight) {
            return ModelErrorUtils.getErrorObject("W_01");
        }
        return getCorrectObject(validatedInteger.value);
    };

    function getCorrectObject(val) { return { isValid: true, value: val }; }

    /**
     * Initialize the addon.
     * For now the height is set to addon height minus 37 which is TinyMCE toolbar height.
     * It was not possible to get that value in easy and dynamic way and it didn't make sense
     * for prototype purpose. Also the set of controls is static and it could be be moved to
     * configuration.
     */
    presenter.getPlugins = function AddonParagraph_getPlugins() {
        var plugins = [];
        if (presenter.configuration.toolbar.indexOf('forecolor') &gt; -1 ||
            presenter.configuration.toolbar.indexOf('backcolor') &gt; -1 ) {
            plugins.push("textcolor");
        }

        if (presenter.configuration.isPlaceholderSet) {
            plugins.push(presenter.configuration.pluginName);
        }

        return plugins.join(" ");
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradePlaceholderText(model);
        upgradedModel = presenter.upgradeManualGrading(upgradedModel);
        upgradedModel = presenter.upgradeTitle(upgradedModel);
        upgradedModel = presenter.upgradeWeight(upgradedModel);
        upgradedModel = presenter.upgradeModelAnswer(upgradedModel);
        upgradedModel = presenter.upgradeEditablePlaceholder(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeBlockInErrorCheckingMode(upgradedModel);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        return presenter.upgradeUseCustomCSSFiles(upgradedModel);
    };

    presenter.upgradeManualGrading = function (model) {
        return presenter.upgradeAttribute(model, "Manual grading", false);
    };

    presenter.upgradeTitle = function (model) {
        return presenter.upgradeAttribute(model, "Title", "");
    };

    presenter.upgradePlaceholderText = function (model) {
        return presenter.upgradeAttribute(model, "Placeholder Text", "");
    };

    presenter.upgradeEditablePlaceholder = function (model) {
        return presenter.upgradeAttribute(model, "Editable placeholder", "");
    };

    presenter.upgradeWeight = function (model) {
        return presenter.upgradeAttribute(model, "Weight", "");
    };

    presenter.upgradeModelAnswer = function (model) {
        const upgradedModel = presenter.upgradeAttribute(model, "Show Answers", [{Text: ""}]);

        // for backward compatibility where modal answer was single string and now is Array of strings we need to upgrade model
        if (!Array.isArray(upgradedModel["Show Answers"])) {
            upgradedModel["Show Answers"] = [{Text: upgradedModel["Show Answers"]}];
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        return presenter.upgradeAttribute(model, "langAttribute", "");
    };

    presenter.upgradeBlockInErrorCheckingMode = function (model) {
        return presenter.upgradeAttribute(model, "Block in error checking mode", "False");
    };

    presenter.upgradeSpeechTexts = function (model) {
        let defaultValue = {
            Bold: {Bold: ""},
            Italic: {Italic: ""},
            Underline: {Underline: ""},
            AlignLeft: {AlignLeft: ""},
            AlignCenter: {AlignCenter: ""},
            AlignRight: {AlignRight: ""},
            Justify: {Justify: ""}
        };

        const upgradedModel = presenter.upgradeAttribute(model, "speechTexts", defaultValue);
        if (!upgradedModel.speechTexts.hasOwnProperty("Selected")) {
            upgradedModel.speechTexts.Selected = {Selected: ""};
        }

        if (!upgradedModel.speechTexts.hasOwnProperty("ParagraphContent")) {
            upgradedModel.speechTexts.ParagraphContent = {ParagraphContent: ""};
        }

        if (!upgradedModel.speechTexts.hasOwnProperty("Deselected")) {
            upgradedModel.speechTexts.Deselected = {Deselected: ""};
        }

        return upgradedModel;
    };

    presenter.upgradeUseCustomCSSFiles = function (model) {
        return presenter.upgradeAttribute(model, "useCustomCSSFiles", "False");
    };

    presenter.upgradeAttribute = function (model, attrName, defaultValue) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel.hasOwnProperty(attrName)) {
            upgradedModel[attrName] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.setSpeechTexts = function AddonParagraph_setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            ...presenter.DEFAULT_TTS_PHRASES,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.selected),
            deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.deselected),
            paragraphContent: TTSUtils.getSpeechTextProperty(
                speechTexts.ParagraphContent.ParagraphContent,
                presenter.speechTexts.paragraphContent),
            bold : TTSUtils.getSpeechTextProperty(
                speechTexts.Bold.Bold,
                presenter.speechTexts.bold),
            italic: TTSUtils.getSpeechTextProperty(
                speechTexts.Italic.Italic,
                presenter.speechTexts.italic),
            underline : TTSUtils.getSpeechTextProperty(
                speechTexts.Underline.Underline,
                presenter.speechTexts.underline),
            alignLeft: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignLeft.AlignLeft,
                presenter.speechTexts.alignLeft),
            alignCenter: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignCenter.AlignCenter,
                presenter.speechTexts.alignCenter),
            alignRight: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignRight.AlignRight,
                presenter.speechTexts.alignRight),
            justify: TTSUtils.getSpeechTextProperty(
                speechTexts.Justify.Justify,
                presenter.speechTexts.justify)
        };
    };

    presenter.preDestroy = function() {
        if (!presenter.isAttempted()) {
            sendPreDestroyedEmptyEvent();
        }
    };

    presenter.onDestroy = function AddonParagraph_destroy() {
        // iOS fix to hide keyboard after page change
        // https://github.com/tinymce/tinymce/issues/3441
        try {
            if (isIOSSafari()) {
                var iframe = presenter.$view.find('iframe');
                iframe.focus();
                document.activeElement.blur();
            }

            clearTimeout(presenter.toolbarChangeHeightTimeoutID);
            clearTimeout(presenter.paragraphInitTimeoutID);
            presenter.toolbarChangeHeightTimeoutID = null;

            presenter.placeholder = null;
            presenter.editor.destroy();
            presenter.jQueryTinyMCEHTML.off();

            presenter.$view.off();
            presenter.$tinyMCEToolbar.off();

            tinymce.AddOnManager.PluginManager.items.length = 0;
            presenter.$tinyMCEToolbar = null;
            presenter.jQueryTinyMCEHTML = null;
            presenter.configuration = null;
            presenter.$view = null;
            presenter.view = null;
            presenter.editor = null;
            presenter.isVisibleValue = null;
            presenter.findIframeAndSetStyles = null;
            presenter.getSpecifyToolbar = null;
            presenter.addStylesToButton = null;
            presenter.getButton = null;
            presenter.onBlur = null;
            presenter.onFocus = null;
            presenter.onInit = null;
            presenter.setIframeHeight = null;
            presenter.destroy = null;
            presenter.tinyMceContainer = null;
            presenter.editor = null;
            presenter.playerController = null;
            presenter.LANGUAGES = null;
            presenter.setWrapperID = null;
        } catch (e) {
            // In case that the first layout is different than the default one
            // the addon may not fully initialize before onDestroy is called
        }
    };

    presenter.addPlugins = function AddonParagraph_addPlugins() {
        if (presenter.configuration.isPlaceholderSet) {
            presenter.addPlaceholderPlugin();
        }
    };

    presenter.makePluginName = function AddonParagraph_makePluginName(addonID) {
        var name = 'placeholder';
        addonID.replace(/[a-z0-9]+/gi, function(x) {
            name += "_" + x;
        });

        return name;
    };

     presenter.onFocus = function AddonParagraph_onFocus() {
        if (presenter.placeholder.isSet) {
            presenter.placeholder.removePlaceholder();
            presenter.placeholder.shouldBeSet = (presenter.placeholder.getEditorContent() == "");
        }
    };

     presenter.onBlur = function AddonParagraph_onBlur() {
        if (presenter.placeholder.shouldBeSet) {
            presenter.placeholder.addPlaceholder();
        } else {
            presenter.placeholder.removePlaceholder();
        }
    };

    presenter.addPlaceholderPlugin = function AddonParagraph_addPlaceholderPlugin() {
        tinymce.PluginManager.add(presenter.configuration.pluginName, function(editor) {
            editor.on('init', function () {
                presenter.placeholder.init(editor.id);
                editor.on('blur', presenter.onBlur);
                editor.on('focus', presenter.onFocus);
            });
        });
    };

    presenter.placeholderElement = function AddonParagraph_placeholderElement() {
        this.isSet = true;
        this.shouldBeSet = false;
        this.placeholderText = presenter.configuration.isPlaceholderEditable ? "" : presenter.configuration.placeholderText;
        this.contentAreaContainer = null;
        this.el = null;
        this.attrs = {style: {position: 'absolute', top:'5px', left:0, color: '#888', padding: '1%', width:'98%', overflow: 'hidden'} };
    };

    presenter.placeholderElement.prototype.init = function AddonParagraph_placeholderElement_init() {
        this.contentAreaContainer = presenter.editor.getBody();
        this.el = presenter.editor.dom.add(this.contentAreaContainer, "placeholder", this.attrs, this.placeholderText);

        tinymce.DOM.setStyle(this.contentAreaContainer, 'position', 'relative');
        tinymce.DOM.addClass(this.el, "placeholder");
    };

    presenter.placeholderElement.prototype.addPlaceholder = function AddonParagraph_addPlaceholder() {
        this.el = presenter.editor.dom.add(this.contentAreaContainer, "placeholder", this.attrs, this.placeholderText);
        presenter.editor.dom.addClass(this.el, "placeholder");
        this.isSet = true;
        presenter.setStyles();
    };

    presenter.placeholderElement.prototype.setPlaceholderAfterEditorChange = function AddonParagraph_setPlaceholderAfterEditorChange() {
        if (this.getEditorContent() == "") {
            this.shouldBeSet = true;
        } else {
            this.shouldBeSet = false;
            this.removePlaceholder();
        }
    };

    presenter.placeholderElement.prototype.removePlaceholder = function AddonParagraph_removePlaceholder() {
        this.isSet = false;
        presenter.editor.dom.remove(this.el);
    };

    presenter.placeholderElement.prototype.getEditorContent = function AddonParagraph_getEditorContent() {
        return presenter.editor.getContent();
    };

    presenter.onTinymceChange = function AddonParagraph_onTinymceChange(editor, event) {
        if (presenter.configuration.isPlaceholderSet) {
            presenter.placeholder.setPlaceholderAfterEditorChange();
        }
    };

     presenter.getSpecifyToolbar = function AddonParagraph_getSpecifyToolbar(language) {
        var toolbar = "";

        if (language === "French") {
            toolbar = "customBold customItalic customUnderline numlist bullist alignleft aligncenter alignright alignjustify";
        }

        return toolbar;
    };

     presenter.addStylesToButton =  function AddonParagraph_addStylesToButton() {
        var boldButton = presenter.$view.find("[aria-label='" + presenter.getButton("Bold").title + "'] button"),
            italicButton = presenter.$view.find("[aria-label='" + presenter.getButton("Italic").title + "'] button"),
            underlineButton = presenter.$view.find("[aria-label='" + presenter.getButton("Underline").title + "'] button");

        boldButton.css({'font-family': 'tinymce', 'font-size': '18px', 'font-weight': 'bold'});
        italicButton.css({'font-family': 'tinymce', 'font-size': '18px', 'font-weight': 'bold', 'font-style': 'italic'});
        underlineButton.css({'font-family': 'tinymce', 'font-size': '18px', 'font-weight': 'bold', 'text-decoration': 'underline'});
    };

     presenter.getButton = function AddonParagraph_getButton(type) {
        var layoutLanguage = presenter.configuration.layoutType;

        var french = {
            Bold: {
                text: '\u0047',
                title: 'Bold'
            },
            Underline: {
                text: 'S',
                title: 'Underline'
            },
            Italic: {
                text: 'I',
                title: 'Italic'
            }
        };

        var languages = {
            French: french
        };

        return languages[layoutLanguage][type];
    };

     presenter.createButton = function AddonParagraph_createButton(editor, type) {
        var button = presenter.getButton(type);

        return {
            text: button.text,
            title: button.title,
            icon: false,
            onclick: function() {
                editor.execCommand(type);
            }
        };
    };

    presenter.setup = function AddonParagraph_setup(ed) {
        if (presenter.editor == null) {
            presenter.editor = ed;
        }

        ed.on("NodeChange", presenter.onNodeChange);
        ed.on("keyup", presenter.onTinymceChange);
        if (presenter.configuration.layoutType === "French") {
            ed.addButton('customBold', presenter.createButton(this, "Bold"));
            ed.addButton('customItalic', presenter.createButton(this, "Italic"));
            ed.addButton('customUnderline', presenter.createButton(this, "Underline"));
        }
    };

    presenter.onNodeChange = function AddonParagraph_onNodeChange() {
        presenter.setStyles();
    };

    presenter.setStyles = function AddonParagraph_setStyles() {
        if (presenter.editor == null) {
            return;
        }

        var hasDefaultFontFamily = presenter.configuration.hasDefaultFontFamily,
            hasDefaultFontSize = presenter.configuration.hasDefaultFontSize,
            hasContentCss = !ModelValidationUtils.isStringEmpty(presenter.configuration.content_css);

        if (!hasDefaultFontFamily || !hasDefaultFontSize || !hasContentCss) {
            var elements = [ presenter.editor.dom.$('p'), presenter.editor.dom.$('ol'), presenter.editor.dom.$('ul'), presenter.editor.dom.$("placeholder")];

            for (var i = 0; i &lt; elements.length; i++) {
                if (elements[i].length == 0) {
                    continue;
                }

                if (!hasDefaultFontFamily || !hasContentCss) {
                    elements[i].css('font-family', presenter.configuration.fontFamily);
                }

                if (!hasDefaultFontSize || !hasContentCss) {
                    elements[i].css('font-size', presenter.configuration.fontSize);
                }
            }
        }
    };

    presenter.setIframeHeight = function AddonParagraph_setIframeHeight() {
        if (presenter.$view == null) {
            return;
        }

        var $editor = presenter.$view.find('#' + presenter.editor.id + '_ifr'),
            editorHeight = presenter.$view.height();

        if (!presenter.configuration.isToolbarHidden) {
            //setTimouts for checking if height of the toolbar changed
            presenter.toolbarChangeHeightTimeoutID = setTimeout(function () {
                var lastHeight = presenter.$view.find('.mce-toolbar').height(),
                    newHeight,
                    counter = 0,
                    originalEditorHeight = editorHeight;

                editorHeight -= presenter.$view.find('.mce-toolbar').height();
                $editor.height(editorHeight);
                (function checkHeight(){
                    newHeight = presenter.$view.find('.mce-toolbar').height();
                    if(lastHeight !== newHeight) {
                        var height = originalEditorHeight - presenter.$view.find('.mce-toolbar').height();
                        $editor.height(height);
                    }
                    lastHeight = newHeight;

                    if(presenter.toolbarChangeHeightTimeoutID) {
                        clearTimeout(presenter.toolbarChangeHeightTimeoutID);
                    }

                    counter++;
                    if(counter &lt; 3) {
                        presenter.toolbarChangeHeightTimeoutID = setTimeout(checkHeight, 500);
                    }
                })();
            }, 0);
        } else {
            $editor.height(editorHeight);
        }
    };

    presenter.onInit = function AddonParagraph_onInit() {
        if (presenter.configuration.isToolbarHidden) {
            presenter.$view.find('.mce-container.mce-panel.mce-first').remove();
            presenter.$view.find('.mce-edit-area').css('border-top-width', '0');
        }

        presenter.jQueryTinyMCEHTML = $(presenter.editor.dom.select('html'));
        presenter.jQueryTinyMCEHTML.click(function editorDOMSelectClick() {
            presenter.editor.contentWindow.focus();
            $(presenter.editor.contentDocument).find('body').focus();
        });

        presenter.editor.dom.loadCSS(DOMOperationsUtils.getResourceFullPath(presenter.playerController, "addons/resources/style.css"));
        presenter.editor.dom.$("body").css("height", "100%");

        presenter.setStyles();
        if (presenter.configuration.state !== undefined) {
            presenter.editor.setContent(presenter.configuration.state, {format : 'raw'});
            presenter.configuration.state = undefined;
            presenter.setVisibility(presenter.isVisibleValue);
        } else {
            presenter.setVisibility(presenter.configuration.isVisible);
        }

        presenter.$tinyMCEToolbar = presenter.$view.find('.mce-toolbar');
        presenter.setIframeHeight();

        presenter.tinyMceContainer = presenter.$view.find('.mce-container.mce-panel.mce-tinymce');
        presenter.tinyMceContainer.css('border', 0);


        if (presenter.configuration.layoutType === "French") {
            presenter.addStylesToButton();
        }

        if (presenter.configuration.isPlaceholderEditable &amp;&amp; presenter.state == null) {
            presenter.setText(presenter.configuration.placeholderText);
        }

        presenter.addEventListenerOnKeyEscapeToEditorMCE();
    };

    presenter.addEventListenerOnKeyEscapeToEditorMCE = function EditableWindow_addEventListenerOnKeyEscapeToEditorMCE (){
        const mceIframe = presenter.$view.find('.mce-edit-area')[0].childNodes[0];
        const content = (mceIframe.contentDocument || mceIframe.contentWindow.document).documentElement;
        const escapeKeyCallback = function (e) {
            if (e.keyCode === presenter.keys.ESCAPE &amp;&amp; presenter.keyboardControllerObject.keyboardNavigationActive) {
                presenter.dispatchEscapeKeydownEvent();
                document.activeElement.blur();
            }
        };

        content.addEventListener("keydown", escapeKeyCallback);
    };

    presenter.dispatchEscapeKeydownEvent = function EditableWindow_dispatchEscapeKeydownEvent () {
        const event = new KeyboardEvent('keydown', {
            code: 'Escape',
            key: 'Escape',
            charCode: presenter.keys.ESCAPE,
            keyCode: presenter.keys.ESCAPE,
            bubbles: true
        });
        document.body.dispatchEvent(event);
    };

    presenter.setPlayerController = function AddonParagraph_setPlayerController(controller) {
        presenter.playerController = controller;
        presenter.currentPageIndex = presenter.playerController.getCurrentPageIndex();
        presenter.pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
    };

    presenter.getState = function AddonParagraph_getState() {
        var tinymceState;
        if (presenter.configuration.state !== undefined) {
            tinymceState = presenter.configuration.state;
        } else if (presenter.editor != undefined &amp;&amp; presenter.editor.hasOwnProperty("id")) {
            try{
                const isShowAnswersActive = presenter.isShowAnswersActive;
                if (isShowAnswersActive) presenter.hideAnswers();
                tinymceState = presenter.editor.getContent({format : 'raw'});
                if (isShowAnswersActive) presenter.showAnswers();
            }catch(err) {
                return  presenter.state;
            }
        } else {
            tinymceState = '';
        }

        return JSON.stringify({
            'tinymceState' : tinymceState,
            'isVisible' : presenter.isVisibleValue,
            'isLocked' : presenter.isLocked
        });
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state).tinymceState;
    };

    presenter.setState = function AddonParagraph_setState(state) {
        var parsedState = JSON.parse(state),
            tinymceState = parsedState.tinymceState;

        presenter.isVisibleValue = parsedState.isVisible;
        if (presenter.editor != null &amp;&amp; presenter.editor.initialized &amp;&amp; presenter.isEditorLoaded) {
            presenter.setVisibility(presenter.isVisibleValue);
        }

        if (tinymceState!=undefined &amp;&amp; tinymceState!="" &amp;&amp; !isPlaceholderClassInHTML(tinymceState)) {
            if (presenter.editor != null &amp;&amp; presenter.editor.initialized &amp;&amp; presenter.isEditorLoaded) {
                presenter.editor.setContent(tinymceState, {format: 'raw'});
                presenter.state = state;
            } else {
                presenter.configuration.state = tinymceState;
                presenter.state = state;
            }
        }

        if (parsedState.isLocked) {
            presenter.lock();
        } else {
            presenter.unlock();
        }
    };

    function isPlaceholderClassInHTML (html) {
        return html.indexOf("class=\"placeholder\"") !== -1;
    }

    presenter.reset = function AddonParagraph_reset() {
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.placeholder.removePlaceholder();
        if (presenter.configuration.isPlaceholderEditable) {
            presenter.setText(presenter.configuration.placeholderText);
        } else {
            presenter.editor.setContent('');
            presenter.setStyles();
        }
        presenter.placeholder.addPlaceholder();
        if (presenter.isLocked) {
            presenter.unlock();
        }
        sendValueChangedEmptyEvent();
    };

    presenter.show = function AddonParagraph_show() {
        presenter.setVisibility(true);
    };

    presenter.hide = function AddonParagraph_hide() {
        presenter.setVisibility(false);
    };

    presenter.isVisible = function AddonParagraph_isVisible() {
        return presenter.isVisibleValue;
    };

    presenter.setText = function(text) {
        if (presenter.editor != null &amp;&amp; presenter.editor.initialized) {
            if (Array.isArray(text)) {
                presenter.editor.setContent(text[0]);
            } else if (typeof text === 'string' || text instanceof String) {
                presenter.editor.setContent(text);
            }
        }
    };

    presenter.lock = function AddonParagraph_lock() {
        if (!presenter.isLocked) {
            var mask = $('&lt;div&gt;').addClass('paragraph-lock');
            presenter.$view.find('#' + presenter.configuration.ID + '-wrapper').append(mask);
            presenter.isLocked = true;
        }
    };

    presenter.unlock = function AddonParagraph_unlock() {
        if (presenter.isLocked) {
            presenter.$view.find('.paragraph-lock').remove();
            presenter.isLocked = false;
        }
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var model = presenter.upgradeModel(model);
        var configuration = presenter.validateModel(model, false);
        var modelAnswers = configuration.modelAnswer;

        var $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        $wrapper.addClass('printable_addon_Paragraph');
        $wrapper.css("left", "0px");
        $wrapper.css("right", "0px");
        $wrapper.css("height", configuration.height + "px");
        $wrapper.css("padding", "10px 10px 10px 0px");
        var $paragraph = $('&lt;div&gt;&lt;/div&gt;');
        $paragraph.css("left", "0px");
        $paragraph.css("right", "0px");
        $paragraph.css("height", "100%");
        $paragraph.css("border", "1px solid");
        $paragraph.css("padding", "10px");

        var innerText = "";
        if (showAnswers) {
            modelAnswers.forEach((answer) =&gt; {
                innerText += answer.Text += "&lt;div&gt;&lt;/div&gt;&lt;br&gt;";
            });
        }
        if (presenter.printableState) {
            innerText = presenter.printableState;
        }
        $paragraph.html(innerText);

        $wrapper.append($paragraph);
        return $wrapper[0].outerHTML;
    };

    presenter.getOpenEndedContent = function () {
        return presenter.getText();
    }

    presenter.getScoreWithMetadata = function() {
        return [{
            userAnswer: presenter.getText(),
            isCorrect: true
        }];
    }

    presenter.didUserAnswer = function (usersAnswer) {
        var parsedAnswer = usersAnswer.replace(/&lt;(.*?)&gt;/g, '').replace(/&amp;nbsp;/g, '');
        return !!parsedAnswer;
    };
    
    function ParagraphKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    ParagraphKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    ParagraphKeyboardController.prototype.constructor = ParagraphKeyboardController;

    presenter.buildKeyboardController = function Paragraph_buildKeyboardController () {
        presenter.keyboardControllerObject = new ParagraphKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function Paragraph_getElementsForKeyboardNavigation() {
        return this.$view.find(".mce-btn, .mce-edit-area");
    };

    presenter.keyboardController = function Paragraph_keyboardController (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    ParagraphKeyboardController.prototype.selectAction = function () {
        if (presenter.isShowAnswersActive
            || presenter.isGradualShowAnswersActive
            || presenter.isErrorCheckingMode) {
            return;
        }

        const el = this.getTarget(this.keyboardNavigationCurrentElement, true);
        if (el.hasClass("mce-edit-area")) {
            presenter.editor.execCommand('mceCodeEditor');
        } else {
            el[0].click();
            document.activeElement.blur();
            presenter.speakSelectedOnAction(el);
        }
        this.mark(this.keyboardNavigationCurrentElement);
    };

    presenter.speakSelectedOnAction = function Paragraph_speakSelectedOnAction (el) {
        if (el.hasClass("mce-active")) {
            presenter.speak(presenter.speechTexts.selected);
        } else {
            presenter.speak(presenter.speechTexts.deselected);
        }
    };

    ParagraphKeyboardController.prototype.mark = function (element) {
        var target = this.getTarget(element, false);
        target.addClass('keyboard_navigation_active_element_important');
        if (target.hasClass("mce-edit-area")) {
            target.addClass('keyboard-navigation-margin');
        }
    };

    ParagraphKeyboardController.prototype.unmark = function (element) {
        var target = this.getTarget(element, false);
        target.removeClass('keyboard_navigation_active_element_important');
        if (target.hasClass("mce-edit-area")) {
            target.removeClass('keyboard-navigation-margin');
        }
    };

    ParagraphKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    ParagraphKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        this.readCurrentElement();
    };

    ParagraphKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    ParagraphKeyboardController.prototype.readCurrentElement = function () {
        const element = this.getTarget(this.keyboardNavigationCurrentElement, false);
        let ariaLabel = element[0].getAttribute("aria-label");
        let text = "";
        if (element.hasClass("mce-btn") &amp;&amp; ariaLabel) {
            const label = ariaLabel.toLowerCase().replace(/\s/gm, "");
            const ttsKey = presenter.TOOLBAR_ARIAS[label];
            if (ttsKey) {
                text = presenter.speechTexts[ttsKey];
            } else {
                text = ariaLabel;
            }
            text = element.hasClass("mce-active") ? `${text} ${presenter.speechTexts.selected}` : text;
        } else if (element.hasClass("mce-edit-area")) {
            let contentToRead = $(presenter.editor.getContent({format : 'html'}));
            if (contentToRead.text().trim().length === 0) {
                text = presenter.speechTexts.paragraphContent;
            } else {
                text = TTSUtils.getTextVoiceArrayFromElement(contentToRead, presenter.configuration.langTag);
            }
        } else {
            let content;
            try {
                content = element[0].textContent;
            } catch (error) {
                console.error(error);
                content = "element";
            }
            text = content;
        }

        presenter.speak(text);
    };

    presenter.speak = function Paragraph_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function Paragraph_setWCAGStatus(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function Paragraph_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.getActivitiesCount = function Paragraph_getActivitiesCount () {
        let answersCount = 0;
        presenter.configuration.modelAnswer.forEach(answer =&gt; {
            if (answer.Text.replace("&lt;br&gt;", "") !== "") {
                answersCount++;
            }
        });
        return answersCount;
    };

    presenter.getMaxScore = function () {
        if (!presenter.configuration.isValid || !presenter.configuration.manualGrading) {
            return 0;
        }
        return presenter.configuration.weight;
    };

    presenter.isAIReady = function() {
        if (!presenter.configuration.isValid
            || !presenter.configuration.manualGrading
            || !presenter.playerController
        ) {
            return false;
        }
        return OpenActivitiesUtils.isAIReady(
            presenter.playerController,
            presenter.pageID,
            presenter.configuration.ID
        );
    };

    presenter.waitForAIGrade = function () {
        presenter.startTime = new Date().getTime();
        presenter.disableEdit();
        presenter.aiGradeInterval = setInterval(function () {
            presenter.checkTimer();
        }, 1000);
    };

    presenter.checkTimer = function () {
        const currentTime = new Date().getTime();
        if ((currentTime - presenter.startTime) / 1_000 &gt;= presenter.MAX_WAIT_TIME) {
            clearInterval(presenter.aiGradeInterval);
            presenter.isWaitingForAIGrade = false;
            presenter.enableEdit();
            DOMOperationsUtils.showErrorMessage(presenter.view, presenter.ERROR_CODES, presenter.configuration.errorCode);
        }
    }

    presenter.handleUpdateScoreEvent = function (eventData) {
        if (eventData.source !== presenter.configuration.ID &amp;&amp; eventData.value !== presenter.updateScoreEventName) { return; }

        clearInterval(presenter.aiGradeInterval);
        presenter.isWaitingForAIGrade = false;
        presenter.enableEdit();
    }

    presenter.isOpenActivity = function () {
        return presenter.configuration.isValid &amp;&amp; presenter.configuration.manualGrading;
    }

    return presenter;
}

AddonParagraph_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Paragraph_Keyboard" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Default font family" nameLabel="Paragraph_Keyboard_property_default_font_family" type="string"/>
		<property name="Default font size" nameLabel="Paragraph_Keyboard_property_default_font_size" type="string"/>
		<property name="Hide toolbar" nameLabel="Paragraph_Keyboard_property_hide_toolbar" type="boolean"/>
		<property name="Editable placeholder" nameLabel="Paragraph_Keyboard_property_editable_placeholder" type="boolean"/>
		<property name="Custom toolbar" nameLabel="Paragraph_Keyboard_property_custom_toolbar" type="string"/>
		<property name="Custom CSS" nameLabel="Paragraph_Keyboard_property_custom_css" type="file"/>
		<property name="useCustomCSSFiles" nameLabel="Paragraph_Keyboard_property_use_custom_css_files" type="boolean"/>
		<property name="Placeholder Text" nameLabel="Paragraph_Keyboard_property_placeholder_text" type="html"/>
		<property displayName="Keyboard Layout Type" name="layoutType" nameLabel="Paragraph_Keyboard_property_layout_type" type="{French (Special Characters), German (Special Characters), Spanish (Special Characters), Custom}"/>
		<property displayName="Custom Keyboard Layout" name="keyboardLayout" nameLabel="Paragraph_Keyboard_property_keyboard_layout" type="text"/>
		<property displayName="Keyboard position" name="keyboardPosition" nameLabel="Paragraph_Keyboard_property_keyboard_position" type="{Bottom, Top, Left, Right, Custom}"/>
		<property name="Manual grading" nameLabel="Paragraph_Keyboard_property_manual_grading" type="boolean"/>
		<property name="Show Answers" nameLabel="Paragraph_Keyboard_property_show_answers" type="html"/>
		<property name="Title" nameLabel="Paragraph_Keyboard_property_title" type="string"/>
		<property name="Weight" nameLabel="Paragraph_Keyboard_property_weight" type="string"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Paragraph_Keyboard_property_speech_texts" type="staticlist">
            <property name="Selected" nameLabel="Paragraph_Keyboard_property_selected" type="staticrow">
                <property name="Selected" nameLabel="Paragraph_Keyboard_property_selected" type="string"/>
            </property>
            <property name="Deselected" nameLabel="Paragraph_Keyboard_property_deselected" type="staticrow">
                <property name="Deselected" nameLabel="Paragraph_Keyboard_property_deselected" type="string"/>
            </property>
			<property name="ParagraphContent" nameLabel="Paragraph_Keyboard_property_ParagraphContent" type="staticrow">
				<property name="ParagraphContent" nameLabel="Paragraph_Keyboard_property_ParagraphContent" type="string"/>
			</property>
			<property name="Bold" nameLabel="Paragraph_Keyboard_property_bold" type="staticrow">
				<property name="Bold" nameLabel="Paragraph_Keyboard_property_bold" type="string"/>
			</property>
			<property name="Italic" nameLabel="Paragraph_Keyboard_property_italic" type="staticrow">
				<property name="Italic" nameLabel="Paragraph_Keyboard_property_italic" type="string"/>
			</property>
			<property name="Underline" nameLabel="Paragraph_Keyboard_property_underline" type="staticrow">
				<property name="Underline" nameLabel="Paragraph_Keyboard_property_underline" type="string"/>
			</property>
			<property name="AlignLeft" nameLabel="Paragraph_Keyboard_property_AlignLeft" type="staticrow">
				<property name="AlignLeft" nameLabel="Paragraph_Keyboard_property_AlignLeft" type="string"/>
			</property>
			<property name="AlignCenter" nameLabel="Paragraph_Keyboard_property_AlignCenter" type="staticrow">
				<property name="AlignCenter" nameLabel="Paragraph_Keyboard_property_AlignCenter" type="string"/>
			</property>
			<property name="AlignRight" nameLabel="Paragraph_Keyboard_property_AlignRight" type="staticrow">
				<property name="AlignRight" nameLabel="Paragraph_Keyboard_property_AlignRight" type="string"/>
			</property>
			<property name="Justify" nameLabel="Paragraph_Keyboard_property_Justify" type="staticrow">
				<property name="Justify" nameLabel="Paragraph_Keyboard_property_Justify" type="string"/>
			</property>
            <property name="Shift" nameLabel="Paragraph_Keyboard_property_Shift" type="staticrow">
				<property name="Shift" nameLabel="Paragraph_Keyboard_property_Shift" type="string"/>
			</property>
		</property>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Paragraph_Keyboard_property_lang_attribute" type="string"/>
	</model>
<css>div.paragraph-keyboard-wrapper {
	width: 100%;
	height: 100%;
    position: relative;
}

div.paragraph-keyboard-wrapper.disabled {
	pointer-events: none;
  	cursor: default;
	opacity: 0.5;
}

div.paragraph-wrapper {
    position: absolute;
    border: solid 1px #9E9E9E;
	border-radius: 2px;
    overflow-y: hidden;
    overflow-x: auto;
}

div.paragraph-wrapper textarea {
	width: 100%;
	height: 100%;
}

div.paragraph-lock {
	width: 100%;
	height: 100%;
	background: #e8eef3;
	position: absolute;
	top: 1px;
}

.paragraph-keyboard {
    position: absolute;
    display: inline-block;
    background: #eeeeee;
    border: solid 1px #dddddd;
    padding: 2px;
    border-radius: 4px;
}

.paragraph-keyboard-letter {
    min-width: 2em;
    width: auto;
    float: left;
    text-align: center;
    padding: 2px;
    color: #1c94c4;
    border: solid 1px #ccc;
    background: #f6f6f6;
    border-radius: 4px;
    cursor: pointer;
    margin: 2px;
}

.paragraph-keyboard-empty {
    min-width: 2em;
    width: auto;
    float: left;
    text-align: center;
    padding: 2px;
    margin: 3px;
}

.paragraph-keyboard-letter.clicked {
    background: #fdf5ce;
    color: #c77405;
    border-color: #fbcb09;
}

.keyRow {
    width: auto;
    float: left;
}

.keyboard-clear {
    clear: both;
    width: auto;
}

.paragraph-keyboard-shift {
    padding: 2px;
    color: #1c94c4;
    border: solid 1px #ccc;
    border-radius: 4px;
    cursor: pointer;
    min-width: 2em;
    width: auto;
    float: left;
    text-align: center;
    padding: 2px;
    margin: 3px;
    background: url("resources/ic_keyboard_capslock_24px.svg") no-repeat center center #f6f6f6;
}

.paragraph-keyboard-shift.clicked {
    background-color: #fdf5ce;
    border-color: #fbcb09;
}
</css><view>&lt;div class="paragraph-keyboard-wrapper"&gt;
	&lt;div class="paragraph-wrapper"&gt;
		&lt;form&gt;
			&lt;textarea class="paragraph_field"&gt;&lt;/textarea&gt;
		&lt;/form&gt;
	&lt;/div&gt;
	&lt;div class="paragraph-keyboard"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="paragraph-keyboard-wrapper"&gt;
	&lt;div class="paragraph-wrapper"&gt;
		&lt;form&gt;
			&lt;textarea class="paragraph_field"&gt;&lt;/textarea&gt;
		&lt;/form&gt;
	&lt;/div&gt;
	&lt;div class="paragraph-keyboard"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonParagraph_Keyboard_create() {
    var presenter = function () {};
    
    var userAnswerAIReviewRequest = 'gradeByAi';
    var userAnswerAIReviewResponse = 'aiGraded';

    presenter.placeholder = null;
    presenter.editor = null;
    presenter.window = null;
    presenter.isVisibleValue = null;
    presenter.view = null;
    presenter.configuration = null;
    presenter.$view = null;
    presenter.jQueryTinyMCEHTML = null;
    presenter.$tinyMCEToolbar = null;
    presenter.$TinyMCEBody = null;
    presenter.cachedAnswer = [];
    presenter.isShowAnswersActive = false;
    presenter.eKeyboardButtons = [];
    presenter.isEditorLoaded = false;
    presenter.currentPageIndex = null;
    presenter.isWaitingForAIGrade = false;
    presenter.aiGradeInterval = null;
    presenter.MAX_WAIT_TIME = 15;
    presenter.startTime = null;
    presenter.updateScoreEventName = 'updateScore';
    presenter.eventBus = null;
    presenter.textParser = null;

    var checkHeightCounter = 0;

    presenter.DEFAULTS = {
        TOOLBAR: 'bold italic underline numlist bullist alignleft aligncenter alignright alignjustify',
        FONT_FAMILY: 'Verdana,Arial,Helvetica,sans-serif',
        FONT_SIZE: '11px',
        BUTTON_WIDTH: 37,
        FORMAT_WIDTH: 85,
        STYLE_SELECT_NAME: "styleselect",
    };

    presenter.ALLOWED_TOOLBAR_BUTTONS = 'customBold customUnderline customItalic newdocument bold italic underline strikethrough alignleft aligncenter '+
        'alignright alignjustify styleselect formatselect fontselect fontsizeselect '+
        'bullist numlist outdent indent blockquote undo redo '+
        'removeformat subscript superscript forecolor backcolor |'.split(' ');

    presenter.ERROR_CODES = {
        'defaultLayoutError' : 'Custom Keyboard Layout should be a JavaScript object with at least "default" property ' +
            'which should be an array of strings with space-seperated chars.',
        'W_01' : 'Weight must be a whole number between 0 and 100',
        'W_02': 'Occurs a problem with checking your answers. Please try later.'
    };

    presenter.LAYOUT_TO_LANGUAGE_MAPPING = {
        'french (special characters)' : "{ \
            'default': ['\u00e0 \u00e2 \u00e7 \u00e8 \u00e9 \u00ea \u00ee \u00ef \u00f4 \u00f9 \u0153 \u00e6 \u00eb {shift}'], \
            'shift': ['\u00c0 \u00c2 \u00c7 \u00c8 \u00c9 \u00ca \u00cb \u00ce \u00cf \u00d4 \u00d9 \u00c6 \u0152 {shift}'] \
        }",
        'german (special characters)' : "{ \
            'default': ['\u00e4 \u00f6 \u00fc \u00df {shift}'], \
            'shift': ['\u00c4 \u00d6 \u00dc {empty} {shift}'] \
        }",
        'spanish (special characters)' : "{ \
            'default': ['\u00e1 \u00e9 \u00ed \u00f3 \u00fa \u00f1 \u00e7 \u00fc \u00a1 \u00bf \u00ba \u00aa {shift}'], \
            'shift': ['\u00c1 \u00c9 \u00cd \u00d3 \u00da \u00d1 \u00c7 \u00dc {empty} {empty} {empty} {empty} {shift}'] \
        }"
    };

    presenter.TOOLBAR_ARIAS = {
        bold: "bold",
        italic: "italic",
        underline: "underline",
        alignleft: "alignLeft",
        aligncenter: "alignCenter",
        alignright: "alignRight",
        justify: "justify"
    };

    presenter.DEFAULT_TTS_PHRASES = {
        selected: "selected",
        deselected:"deselected",
        paragraphContent: "paragraph content",
        bold: "bold",
        italic: "italic",
        underline: "underline",
        alignLeft: "align left",
        alignCenter: "align center",
        alignRight: "align right",
        justify: "justify",
        shift: "shift"
    };

    presenter.CSS_CLASSES = {
        MCE_EDIT_AREA: "mce-edit-area",
        MCE_BUTTON: "mce-btn",
        MCE_ACTIVE: "mce-active",
        KEYBOARD_LETTER: "paragraph-keyboard-letter",
        KEYBOARD_SHIFT: "paragraph-keyboard-shift",
    };

    presenter.validateType = function AddonParagraph_Keyboard_validateType(rawType) {
        if (!rawType || rawType.length == 0) {
            return 'french (special characters)';
        }

        return rawType.toLowerCase();
    };

    presenter.setVisibility = function AddonParagraph_Keyboard_setVisibility(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        if (isVisible) {
            presenter.$view.find(".paragraph-keyboard-wrapper").show();
        } else {
            presenter.$view.find(".paragraph-keyboard-wrapper").hide();
        }

        presenter.isVisibleValue = isVisible;
    };

    presenter.createPreview = function AddonParagraph_Keyboard_createPreview(view, model) {
        presenter.initializeEditor(view, model, true);
        presenter.setVisibility(true);
        var clickhandler = $("&lt;div&gt;&lt;/div&gt;").css({"background":"transparent", 'width': '100%', 'height': '100%', 'position':'absolute', 'top':0, 'left':0});
        presenter.$view.append(clickhandler);
    };

    presenter.setEventBus = function (wrappedEventBus) {
        presenter.eventBus = wrappedEventBus;

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('GradualShowAnswers', this);
        presenter.eventBus.addEventListener('GradualHideAnswers', this);
        presenter.eventBus.addEventListener('ValueChanged', this);
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;

            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "HideAnswers":
            case "GradualHideAnswers":
                presenter.hideAnswers();
                break;

            case "ValueChanged":
                presenter.handleUpdateScoreEvent(eventData);
                break;
        }
    };

    presenter.getActivitiesCount = function () {
        return 1;
    }

    presenter.enableEdit = function () {
        const paragraphKeyboard = presenter.$view.find(".paragraph-keyboard-wrapper");

        if(paragraphKeyboard.hasClass('disabled')) {
            paragraphKeyboard.removeClass('disabled');
        }
    }

    presenter.disableEdit = function () {
        const paragraphKeyboard = presenter.$view.find(".paragraph-keyboard-wrapper");

        if(!paragraphKeyboard.hasClass('disabled')) {
            paragraphKeyboard.addClass('disabled');
        }
    }

    presenter.showAnswers = function () {
        if (presenter.isShowAnswersActive) return;

        const elements = presenter.getParagraphs();

        presenter.disableEdit();
        presenter.isShowAnswersActive = true;

        for (let [key, value] of Object.entries(elements)) {
            if (+key &gt; -1) {
                presenter.cachedAnswer.push(value.innerHTML);
                if (+key === 0) {
                    value.innerHTML = presenter.configuration.modelAnswer;
                } else {
                    value.innerHTML = '';
                }
            }
        }
    }

    presenter.hideAnswers = function () {
        const elements = presenter.getParagraphs();

        presenter.enableEdit();
        presenter.isShowAnswersActive = false;

        if (presenter.cachedAnswer.length) {
            for (let [key, value] of Object.entries(elements)) {
                if (+key &gt; -1) {
                    value.innerHTML = presenter.cachedAnswer[+key];
                }
            }
            presenter.cachedAnswer = [];
        }
    }

    presenter.gradualShowAnswers = function (data) {
        presenter.disableEdit();
        if (data.moduleID !== presenter.configuration.ID) return;
        presenter.showAnswers();
    };

    presenter.sendUserAnswerToAICheck = function () {
        if(!presenter.isAIReady() || presenter.isWaitingForAIGrade) { return; }

        const data = presenter.getDataRequestToAI();

        window.addEventListener("message", presenter.onExternalMessage);
        presenter.playerController.sendExternalEvent(userAnswerAIReviewRequest, data);
        presenter.isWaitingForAIGrade = true;
        presenter.waitForAIGrade();
    };

    presenter.getDataRequestToAI = function () {
        const pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
        const activityID = presenter.configuration.ID;
        const activityMaxScore = presenter.getMaxScore();
        const pageWeight = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getPageWeight();
        const pageMaxScore = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getModulesMaxScore();
        const answer = presenter.getText().replace(/&lt;(.*?)&gt;/g, '').replace(/&amp;nbsp;/g, '');

        return JSON.stringify({
            'page_id': pageID,
            'activity_id': activityID,
            'activity_max_score': activityMaxScore,
            'page_weight': pageWeight,
            'page_max_score': pageMaxScore,
            'answer': answer
        });
    };

    presenter.onExternalMessage = function (event) {
        const data = event.data;

        if (presenter.isValidResponse(data)) {
            presenter.updateOpenActivityScore(data);
        }
    };

    presenter.isValidResponse = function (data) {
        const isAIResponse = data.includes(userAnswerAIReviewResponse);
        if (!isAIResponse) { return isAIResponse; }

        const pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
        const activityID = presenter.configuration.ID;
        const isValidPageID = data.includes(pageID);
        const isValidActivityID = data.includes(activityID);

        return isAIResponse &amp;&amp; isValidPageID &amp;&amp; isValidActivityID;
    };

    presenter.updateOpenActivityScore = function (data) {
        const parsedData = JSON.parse(data.replace(`EXTERNAL_${userAnswerAIReviewResponse}:`, '').trim());
        const pageID = parsedData.page_id;
        const activityID = parsedData.activity_id;
        const aiGrade = parsedData.ai_grade;
        const aiRelevance = parsedData.ai_relevance;

        OpenActivitiesUtils.updateOpenActivityScore(
            presenter.playerController,
            pageID,
            activityID,
            aiGrade,
            aiRelevance
        );
    };

    presenter.setShowErrorsMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
    };

    presenter.setWorkMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
    };

    presenter.getParagraphs = function () {
        const paragraph = presenter.$view.find(".paragraph-wrapper"),
            iframe = paragraph.find("iframe"),
            body = $(iframe).contents().find("#tinymce");

        return body.find("p");
    };

    presenter.run = function AddonParagraph_Keyboard_run(view, model) {
        presenter.initializeEditor(view, model, false);
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.isLocked = false;
    };

    presenter.validateToolbar = function AddonParagraph_validateToolbar(controls, width) {
        if (!controls) {
            controls = presenter.DEFAULTS.TOOLBAR;
        }

        controls = controls.split(" ");
        if (controls.indexOf("|") != -1) {
            return presenter.parseToolbarWithGroups(controls, width);
        } else {
            return presenter.parseToolbarWithoutGroups(controls, width);
        }
    };

    presenter.parseToolbarWithGroups = function (controls, toolbarWidth) {
        var controlGroups = controls.join(" ").split("|");
        return controlGroups.filter(function (group) {
            return group.trim().length &gt; 0;
        }).map(function (group) {
            return presenter.parseToolbarWithoutGroups(group.trim().split(" "), toolbarWidth);
        }).join(" | ");
    };

    presenter.parseToolbarWithoutGroups = function (controls, toolbarWidth) {
        var filteredControls = controls.filter(function(param){
            return presenter.ALLOWED_TOOLBAR_BUTTONS.indexOf(param) != -1;
        });

        var result = "";
        var bufor = 0;
        var widthToAdd = 0;
        for(var i = 0; i &lt; filteredControls.length; i++) {
            if (filteredControls[i] !== presenter.DEFAULTS.STYLE_SELECT_NAME) {
                widthToAdd = presenter.DEFAULTS.BUTTON_WIDTH;
            } else {
                widthToAdd = presenter.DEFAULTS.FORMAT_WIDTH;
            }

            if (bufor + widthToAdd &lt; toolbarWidth) {
                bufor += widthToAdd;
                result += filteredControls[i].trim() + " ";
            } else {
                bufor = widthToAdd;
                result += "| " + filteredControls[i].trim() + " ";
            }
        }

        return result.trim();
    };

    function pasteHtmlAtCaret(html, wnd, ownerDocument) {
        var sel, range;
        if (wnd.getSelection) {
            // IE9 and non-IE
            sel = wnd.getSelection();
            if (sel.getRangeAt &amp;&amp; sel.rangeCount) {
                range = sel.getRangeAt(0);
                range.deleteContents();

                // Range.createContextualFragment() would be useful here but is
                // only relatively recently standardized and is not supported in
                // some browsers (IE9, for one)
                var el = document.createElement("div");
                el.innerHTML = html;
                var frag = document.createDocumentFragment(), node, lastNode;

                // fix for IE
                if (ownerDocument) {
                    frag = ownerDocument.createDocumentFragment()
                }

                while ( (node = el.firstChild) ) {
                    lastNode = frag.appendChild(node);
                }
                range.insertNode(frag);

                // Preserve the selection
                if (lastNode) {
                    range = range.cloneRange();
                    range.setStartAfter(lastNode);
                    range.collapse(true);
                    sel.removeAllRanges();
                    sel.addRange(range);
                }
            }
        } else if (document.selection &amp;&amp; document.selection.type != "Control") {
            // IE &lt; 9
            document.selection.createRange().pasteHTML(html);
        }
    }

     function transposeLayout(layout){
        var newLayout = {};
        $.each(layout, function(name,keyset){
            var ar = [];
            for (var i=0; i &lt; keyset.length; i++) {
                var row = keyset[i].split(' ');
                for (var j=0; j &lt; row.length; j++) {
                    if (!ar[j]) ar[j] = [];
                    ar[j][i] = row[j];
                }
            }
            for (var k=0; k &lt; ar.length; k++) {
                ar[k] = ar[k].join(' ');
            }
            newLayout[name] = ar;
        });
        return newLayout;
    }

    /**
     * Parses model and set settings to default values if either of them is empty
     *
     * IMPORTANT. Validation resulting in a validation error, e.g. W_01, must be implemented in Open Activities
     * on the mCourser side.
     *
     * @param model:object
     * @param isPreview:boolean
     * @returns {{fontFamily: *, fontSize: *}}
     */
    presenter.parseModel = function AddonParagraph_Keyboard_parseModel(model, isPreview) {
        var fontFamily = model['Default font family'],
            fontSize = model['Default font size'],
            isToolbarHidden = ModelValidationUtils.validateBoolean(model['Hide toolbar']),
            isPlaceholderEditable = ModelValidationUtils.validateBoolean(model['Editable placeholder']),
            toolbar = presenter.validateToolbar(model['Custom toolbar'], model["Width"]),
            height = model.Height,
            hasDefaultFontFamily = false,
            hasDefaultFontSize = false,
            keyboardPosition = model['keyboardPosition'] ? model['keyboardPosition'].toLowerCase() : 'bottom',
            layoutType = presenter.validateType(model['layoutType']),
            keyboardLayout = model['keyboardLayout'],
            title = model["Title"],
            manualGrading = ModelValidationUtils.validateBoolean(model["Manual grading"]),
            weight = model['Weight'],
            modelAnswer = model['Show Answers'];

        if (ModelValidationUtils.isStringEmpty(fontFamily)) {
            fontFamily = presenter.DEFAULTS.FONT_FAMILY;
            hasDefaultFontFamily = true;
        }

        if (ModelValidationUtils.isStringEmpty(fontSize)) {
            fontSize = presenter.DEFAULTS.FONT_SIZE;
            hasDefaultFontSize = true;
        }

        height -= !isToolbarHidden ? 37 : 2;

        const validatedWeight = presenter.validateWeight(weight, isPreview);
        if (!validatedWeight.isValid) {
            return validatedWeight;
        }

        const supportedPositions = ['top', 'bottom', 'custom', 'left', 'right'];
        if (supportedPositions.indexOf(keyboardPosition) === -1) {
            keyboardPosition = 'bottom';
        }

        const validatedLayout = presenter.validateLayout(keyboardLayout, layoutType, keyboardPosition);
        if (!validatedLayout.isValid) {
            return validatedLayout;
        }

        return {
            ID: model["ID"],
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isValid: true,

            fontFamily: fontFamily,
            fontSize: fontSize,
            isToolbarHidden: isToolbarHidden,
            toolbar: toolbar,
            textAreaHeight: height,
            paragraphHeight: model.Height,
            width: model['Width'],
            hasDefaultFontFamily: hasDefaultFontFamily,
            hasDefaultFontSize: hasDefaultFontSize,
            content_css: model['Custom CSS'],
            useCustomCSSFiles: ModelValidationUtils.validateBoolean(model["useCustomCSSFiles"]),
            isPlaceholderSet: !ModelValidationUtils.isStringEmpty(model["Placeholder Text"]),
            placeholderText: model["Placeholder Text"],
            isPlaceholderEditable: isPlaceholderEditable,
            pluginName: presenter.makePluginName(model["ID"]),
            keyboardLayout: validatedLayout.value,
            keyboardPosition: keyboardPosition,
            manualGrading: manualGrading,
            title: title,
            weight: validatedWeight.value,
            modelAnswer: modelAnswer,
            langTag: model["langAttribute"]
        };
    };

    presenter.validateLayout = function (keyboardLayout, layoutType, keyboardPosition) {
        if (presenter.LAYOUT_TO_LANGUAGE_MAPPING[layoutType]) {
            keyboardLayout = presenter.LAYOUT_TO_LANGUAGE_MAPPING[layoutType];
        }

        const altTextsMap = {};
        if (keyboardLayout.length &gt; 0) {
            keyboardLayout = replaceAltText(keyboardLayout, altTextsMap);
            try {
                eval('keyboardLayout = ' + keyboardLayout);
            } catch(e) {
                presenter.ERROR_CODES['evaluationError'] = 'Custom keyboard layout parsing error: ' + e.message;
                return ModelErrorUtils.getErrorObject('evaluationError');
            }
        }

        if (!Array.isArray(keyboardLayout['default']) || keyboardLayout['default'].length &lt; 1) {
            return ModelErrorUtils.getErrorObject('defaultLayoutError');
        }

        if (['left', 'right'].includes(keyboardPosition)) {
            keyboardLayout = transposeLayout(keyboardLayout);
        }

        for (let [set, keyset]of Object.entries(keyboardLayout)) {
            let parsedKeyset = [];
            for ( let rowIndex = 0; rowIndex &lt; keyset.length; rowIndex++ ){
                const parsedKeysetRow = [];
                let currentKeysetRow = keyset[rowIndex];
                currentKeysetRow = $.trim(currentKeysetRow);
                const keys = currentKeysetRow.split(/\s+/);

                for ( let keyIndex = 0; keyIndex &lt; keys.length; keyIndex++ ) {
                    const key = keys[keyIndex];
                    if (hasMoreThenOneAltText(key)) {
                        presenter.ERROR_CODES['evaluationError'] = 'Alt texts should be separated by space';
                        return ModelErrorUtils.getErrorObject('evaluationError');
                    }
                    if (altTextsMap[key] !== undefined) {
                        if (hasWhiteSpace(altTextsMap[key].contents)) {
                            presenter.ERROR_CODES['evaluationError'] = 'Text with alternative text has white space: ' + altTextsMap[key].match;
                            return ModelErrorUtils.getErrorObject('evaluationError');
                        }
                        parsedKeysetRow.push(altTextsMap[key].match);
                    } else {
                        parsedKeysetRow.push(key);
                    }
                }
                parsedKeyset.push(parsedKeysetRow);
            }
            keyboardLayout[set] = parsedKeyset;
        }

         return {
            isValid: true,
            value: keyboardLayout,
            errorCode: ''
        };
    };

    function replaceAltText (text, altTextsMap) {
        let altTextIndex = 0;
        return text.replace(/\\alt{([^{}|]*?)\|[^{}|]*?}(\[[a-zA-Z0-9_\- ]*?\])*/g, function (match, contents, offset, string, groups) {
            altTextIndex++;
            const newIndex = "{altText" + altTextIndex + "}";
            altTextsMap[newIndex] = {
                match: match,
                contents: contents.trim()
            };
            return newIndex;
        });
    }

    function hasWhiteSpace(s) {
        return /\s/g.test(s);
    }

    function hasMoreThenOneAltText(s) {
        return (s.match(new RegExp(/\{altText[0-9]+\}/, "g")) || []).length &gt; 1;
    }

    presenter.validateWeight = function (weight, isPreview) {
        if (ModelValidationUtils.isStringEmpty(weight)) {
            return getCorrectObject(1);
        }

        const validatedInteger = ModelValidationUtils.validateIntegerInRange(weight, 100, 0);
        if (!validatedInteger.isValid) {
            return ModelErrorUtils.getErrorObject("W_01");
        }
        if (isPreview &amp;&amp; (validatedInteger.value + "") !== weight) {
            return ModelErrorUtils.getErrorObject("W_01");
        }
        return getCorrectObject(validatedInteger.value);
    };

    function getCorrectObject(val) { return { isValid: true, value: val }; }

    presenter.setWrapperID = function AddonParagraph_Keyboard_setWrapperID() {
        var $paragraphWrapper = presenter.$view.find('.paragraph-wrapper');
        $paragraphWrapper.attr('id', presenter.configuration.ID + '-wrapper');
    };

    presenter.upgradeAttribute = function (model, attrName, defaultValue) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (model[attrName] == undefined) {
            upgradedModel[attrName] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.getPlugins = function AddonParagraph_Keyboard_getPlugins() {
        var plugins = [];
        if (presenter.configuration.toolbar.indexOf('forecolor') &gt; -1 ||
            presenter.configuration.toolbar.indexOf('backcolor') &gt; -1 ) {
            plugins.push("textcolor");
        }

        if (presenter.configuration.isPlaceholderSet) {
            plugins.push(presenter.configuration.pluginName);
        }

        return plugins.join(" ");
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeTitle(model);
        upgradedModel = presenter.upgradeManualGrading(upgradedModel);
        upgradedModel = presenter.upgradeWeight(upgradedModel);
        upgradedModel = presenter.upgradeModelAnswer(upgradedModel);
        upgradedModel = presenter.upgradePlaceholderText(upgradedModel);
        upgradedModel = presenter.upgradeEditablePlaceholder(upgradedModel);
        upgradedModel = presenter.upgradeUseCustomCSSFiles(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.upgradeManualGrading = function (model) {
        return presenter.upgradeAttribute(model, "Manual grading", false);
    };

    presenter.upgradeTitle = function (model) {
        return presenter.upgradeAttribute(model, "Title", "");
    };

    presenter.upgradePlaceholderText = function (model) {
        return presenter.upgradeAttribute(model, "Placeholder Text", "");
    };

    presenter.upgradeEditablePlaceholder = function (model) {
        return presenter.upgradeAttribute(model, "Editable placeholder", "");
    };

    presenter.upgradeWeight = function (model) {
        return presenter.upgradeAttribute(model, "Weight", "");
    };

    presenter.upgradeModelAnswer = function (model) {
        return presenter.upgradeAttribute(model, "Show Answers", "");
    };

    presenter.upgradeUseCustomCSSFiles = function (model) {
        return presenter.upgradeAttribute(model, "useCustomCSSFiles", "False");
    };

    presenter.upgradeLangTag = function (model) {
        return presenter.upgradeAttribute(model, "langAttribute", "");
    };

    presenter.upgradeSpeechTexts = function (model) {
        const defaultValue = {
            Selected: {Selected: ""},
            Deselected: {Deselected: ""},
            ParagraphContent: {ParagraphContent: ""},
            Bold: {Bold: ""},
            Italic: {Italic: ""},
            Underline: {Underline: ""},
            AlignLeft: {AlignLeft: ""},
            AlignCenter: {AlignCenter: ""},
            AlignRight: {AlignRight: ""},
            Justify: {Justify: ""}
        };

        const upgradedModel =  presenter.upgradeAttribute(model, "speechTexts", defaultValue);
        if (!upgradedModel.speechTexts.hasOwnProperty("Shift")) {
            upgradedModel.speechTexts.Shift = {Shift: ""};
        }

        return upgradedModel;
    };


    presenter.setSpeechTexts = function (speechTexts) {
        presenter.speechTexts = {
            ...presenter.DEFAULT_TTS_PHRASES
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.selected),
            deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.deselected),
            paragraphContent: TTSUtils.getSpeechTextProperty(
                speechTexts.ParagraphContent.ParagraphContent,
                presenter.speechTexts.paragraphContent),
            bold : TTSUtils.getSpeechTextProperty(
                speechTexts.Bold.Bold,
                presenter.speechTexts.bold),
            italic: TTSUtils.getSpeechTextProperty(
                speechTexts.Italic.Italic,
                presenter.speechTexts.italic),
            underline : TTSUtils.getSpeechTextProperty(
                speechTexts.Underline.Underline,
                presenter.speechTexts.underline),
            alignLeft: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignLeft.AlignLeft,
                presenter.speechTexts.alignLeft),
            alignCenter: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignCenter.AlignCenter,
                presenter.speechTexts.alignCenter),
            alignRight: TTSUtils.getSpeechTextProperty(
                speechTexts.AlignRight.AlignRight,
                presenter.speechTexts.alignRight),
            justify: TTSUtils.getSpeechTextProperty(
                speechTexts.Justify.Justify,
                presenter.speechTexts.justify),
            shift: TTSUtils.getSpeechTextProperty(
                speechTexts.Shift.Shift,
                presenter.speechTexts.shift),
        };
    };

    presenter.initializeEditor = function AddonParagraph_Keyboard_initializeEditor(view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.isPreview = isPreview;
        const upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.parseModel(upgradedModel, isPreview);

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }
        presenter.setSpeechTexts(upgradedModel["speechTexts"]);

        presenter.$view.on('click', function(e){
            e.stopPropagation();
            e.preventDefault();
        });

        presenter.setWrapperID();

        presenter.initTinymce();
    };

    presenter.initTinymce = function () {
        if (presenter.isPreview || !presenter.configuration.useCustomCSSFiles || !presenter.configuration.content_css) {
            presenter.initTinymceWithoutParsingContentCSS();
        } else {
            presenter.initTinymceWithParsingContentCSS();
        }
    };

    presenter.initTinymceWithoutParsingContentCSS = function () {
        _initTinymce(presenter.configuration.content_css);
    };

    presenter.initTinymceWithParsingContentCSS = function () {
        presenter.$view.css("visibility", "hidden");
        URLUtils.parseCSSFile(presenter.playerController, presenter.configuration.content_css)
            .then((newCSSText) =&gt; {
                const newCSS = new File(
                    [newCSSText],
                    `parsed ${presenter.configuration.content_css}.css`,
                    {type: "text/css"}
                );
                return URL.createObjectURL(newCSS);
            }).then((contentCSSURL) =&gt; {
                _initTinymce(contentCSSURL);
            }).catch(() =&gt; {
                console.warn(`Failed to download assets provided in "Custom CSS" file for Paragraph Keyboard ${presenter.configuration.ID} addon`);
            });
    };

    function _initTinymce(contentCSSURL) {
        presenter.placeholder = new presenter.placeholderElement();
        presenter.configuration.plugins = presenter.getPlugins();
        presenter.addPlugins();

        presenter.buildKeyboard();

        presenter.calculateAndSetSizeForAddon();

        tinymce.init(presenter.getTinyMceInitConfiguration(contentCSSURL)).then(function (editors) {
            presenter.editor = editors[0];
            presenter.onInit();
            presenter.isEditorLoaded = true;
            presenter.setStyles();
            presenter.buildKeyboardController();
        });
    }

    /**
     * Calculate and set in configuration new size for addon.
     * For now the height is set to addon height minus 37 which is TinyMCE toolbar height.
     * It was not possible to get that value in easy and dynamic way, and it didn't make sense
     * for prototype purpose. Also, the set of controls is static, and it could be moved to
     * configuration.
     */
    presenter.calculateAndSetSizeForAddon = function AddonParagraph_Keyboard_calculateAndSetAddonSize() {
        var $keyboard = presenter.$view.find('.paragraph-keyboard'),
            $paragraph = presenter.$view.find('.paragraph-wrapper'),
            keyboardPosition = presenter.configuration.keyboardPosition;

        if (keyboardPosition != 'custom') {

            var width, height, offset = {};

            width = parseInt($keyboard.width(), 10);
            height = parseInt($keyboard.height(), 10);
            offset.bottom = parseInt($keyboard.css('padding-bottom'), 10);
            offset.bottom += parseInt($keyboard.css('border-bottom-width'), 10);
            offset.top = parseInt($keyboard.css('padding-top'), 10);
            offset.top += parseInt($keyboard.css('border-top-width'), 10);

            offset.left = parseInt($keyboard.css('padding-left'), 10);
            offset.left += parseInt($keyboard.css('border-left-width'), 10);
            offset.right = parseInt($keyboard.css('padding-right'), 10);
            offset.right += parseInt($keyboard.css('border-right-width'), 10);

            switch (keyboardPosition) {
                case 'top':
                    presenter.configuration.paragraphHeight -= height + 2 * offset.bottom + offset.top + 1;
                    $paragraph.css('top', (height + offset.top + offset.bottom) + 'px');
                    $paragraph.width('100%');
                    break;
                case 'bottom':
                    presenter.configuration.paragraphHeight -= height + offset.bottom + 2 * offset.top - 1;
                    offset.additional = presenter.configuration.isToolbarHidden ? 1 : 0;
                    $keyboard.css('top', (presenter.configuration.paragraphHeight + offset.top - offset.additional) + 'px');
                    $paragraph.width('100%');
                    break;
                case 'left':
                    presenter.configuration.width -= width + offset.right + 1;
                    $paragraph.width(presenter.configuration.width + 'px');
                    $paragraph.css('left', (width + offset.left + offset.right) + 'px');
                    $paragraph.height('100%');
                    break;
                case 'right':
                    presenter.configuration.width -= width + offset.left + offset.right + 2;
                    $paragraph.width(presenter.configuration.width + 'px');
                    $keyboard.css('left', (presenter.configuration.width + offset.left - 1) + 'px');
                    $paragraph.height('100%');
                    break;
            }
        }
    };

    presenter.getTinyMceInitConfiguration = function AddonParagraph_Keyboard_getTinyMceConfiguration(contentCSSURL) {
        return {
            plugins: presenter.configuration.plugins,
            selector : presenter.getTinyMCESelector(),
            width: presenter.configuration.width,
            height: presenter.configuration.paragraphHeight,
            statusbar: false,
            menubar: false,
            toolbar: presenter.configuration.toolbar,
            content_css: contentCSSURL,
            setup: presenter.setup,
        };
    };

    presenter.setup = function AddonParagraph_Keyboard_setup(editor) {
        if (presenter.editor == null) {
            presenter.editor = editor;
        }

        editor.on("NodeChange", presenter.setStyles);
        editor.on("keyup", presenter.onTinymceChange);
    };

    presenter.sendOnBlurEvent = function () {
        const eventData = {
            'source': presenter.configuration.ID,
            'item': '',
            'value': 'blur',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onDestroy = function () {
        try {
            presenter.$view.off();
        } catch (e) {
        }
        try {
            presenter.jQueryTinyMCEHTML.off();
        } catch (e) {
        }
        try {
            presenter.$tinyMCEToolbar.off();
        } catch (e) {
        }
        try {
            presenter.$TinyMCEBody.off();
        } catch (e) {
        }
        try {
            presenter.editor.destroy();
        } catch (e) {
        }
        try {
            tinymce.remove();
        } catch (e) {
        }

        tinymce.AddOnManager.PluginManager.items.length = 0;

        presenter.placeholder = null;
        presenter.$tinyMCEToolbar = null;
        presenter.jQueryTinyMCEHTML = null;
        presenter.$TinyMCEBody = null;
        presenter.configuration = null;
        presenter.$view = null;
        presenter.view = null;
        presenter.editor = null;
        presenter.tinyMceContainer = null;
        presenter.window = null;
        presenter.isVisibleValue = null;
        presenter.ERROR_CODES = null;
        presenter.DEFAULTS = null;
        presenter.LAYOUT_TO_LANGUAGE_MAPPING = null;

        presenter.setWrapperID = null;
        presenter.findIframeAndSetStyles = null;
        presenter.getSpecifyToolbar = null;
        presenter.addStylesToButton = null;
        presenter.getButton = null;
        presenter.onBlur = null;
        presenter.onFocus = null;
        presenter.onInit = null;
        presenter.setIframeHeight = null;
        presenter.setStyles = null;
        transposeLayout = null;
        pasteHtmlAtCaret = null;

        presenter.switchKeyboard = null;
        presenter.clickKeyboard = null;
        presenter.onMouseDownKeyboard = null;
        presenter.buildKeyboard = null;
        presenter.eKeyboardButtons.forEach(function ($button) {
            $button.off();
        });
        presenter.eKeyboardButtons.length = 0;
        presenter.eKeyboardButtons = null;
    };

    presenter.getTinyMCESelector = function AddonParagraph_Keyboard_getTinyMCESelector() {
        return '#' + presenter.configuration.ID + '-wrapper .paragraph_field';
    };

    presenter.setStyles = function AddonParagraph_Keyboard_setStyles() {
        if (presenter.editor == null) {
            return;
        }

        var hasDefaultFontFamily = presenter.configuration.hasDefaultFontFamily,
            hasDefaultFontSize = presenter.configuration.hasDefaultFontSize,
            hasContentCss = !ModelValidationUtils.isStringEmpty(presenter.configuration.content_css);

        if (!hasDefaultFontFamily || !hasDefaultFontSize || !hasContentCss) {
            var elements = [presenter.editor.dom.$('p'), presenter.editor.dom.$('ol'), presenter.editor.dom.$('ul'), presenter.editor.dom.$("placeholder")];

            for (var i = 0; i &lt; elements.length; i++) {
                if (!hasDefaultFontFamily || !hasContentCss) {
                    elements[i].css('font-family', presenter.configuration.fontFamily);
                }

                if (!hasDefaultFontSize || !hasContentCss) {
                    elements[i].css('font-size', presenter.configuration.fontSize);
                }
            }
        }
    };

    presenter.setIframeHeight = function AddonParagraph_Keyboard_setIframeHeight() {
        var $editor = presenter.$view.find('#' + presenter.editor.id + '_ifr'),
            editorHeight = presenter.configuration.paragraphHeight;

        if (!presenter.configuration.isToolbarHidden) {
            editorHeight -=  presenter.$view.find('.mce-toolbar').height();
        }

        $editor.height(editorHeight);
    };


    presenter.caret = function AddonParagraph_Keyboard_caret() {
        var caretData;
        if (arguments.length) {
            caretData = arguments[0];
            presenter.window.getSelection().collapse(caretData.start.node, caretData.start.offset);
            if (caretData.range) {
                caretData.range.deleteContents();
            }
        } else {
            var selection = presenter.window.getSelection(),
                start = {
                    offset: selection.anchorOffset,
                    node: selection.anchorNode
                },
                range = false;

            if (selection.rangeCount &gt; 0) {
                range = selection.getRangeAt(0);
            }

            caretData = {
                start : start,
                range: range
            };
            return caretData;
        }
    };

    presenter.clickKeyboard = function AddonParagraph_Keyboard_clickKeyboard(e){
        e.stopPropagation();
        e.preventDefault();
        var $this = $(this),
            text = $this.text();
        const wasEditablePlaceholderSet = isEditablePlaceholderSet();

        presenter.window.focus();
        $(presenter.editor.contentDocument).find('body').focus();

        if (wasEditablePlaceholderSet) {
            setCaretOnEndOfEditorLastElement();
        }

        if (presenter.lastCaret) {
            // in IE 11 we have to set caret's position manually, because by default it is set at the beginning
            presenter.caret(presenter.lastCaret);
            pasteHtmlAtCaret(text, presenter.window, presenter.ownerDocument);
            presenter.lastCaret = presenter.caret();
        } else {
            pasteHtmlAtCaret(text, presenter.window, presenter.ownerDocument);
        }

        $this.addClass('clicked');
        window.setTimeout(function(){
            $this.removeClass('clicked');
        }, 200);
    };

    function isEditablePlaceholderSet() {
        return (presenter.placeholder.isSet
            &amp;&amp; presenter.configuration.isPlaceholderEditable
            &amp;&amp; presenter.configuration.isPlaceholderSet
            &amp;&amp; !!$(presenter.getText()).text()
        );
    }

    function setCaretOnEndOfEditorLastElement() {
        var selection = presenter.window.getSelection();
        if (selection.getRangeAt &amp;&amp; selection.rangeCount) {
            var range = selection.getRangeAt(0);
            range.setStartAfter(range.endContainer);
        }
    }

    presenter.switchKeyboard = function AddonParagraph_Keyboard_switchKeyboard(e) {
        e.stopPropagation();
        e.preventDefault();
        presenter.$view.find('.keySetLayer:visible').hide();
        presenter.currentKeyboard = (presenter.currentKeyboard == 'default' ? 'shift' : 'default');
        presenter.$view.find('.keyset-' + presenter.currentKeyboard).show();
        presenter.$view.find(`.${presenter.CSS_CLASSES.KEYBOARD_SHIFT}:visible`).addClass('clicked');

        window.setTimeout(function(){
            presenter.$view.find(`.${presenter.CSS_CLASSES.KEYBOARD_SHIFT}.clicked`).removeClass('clicked');
        }, 200);

        presenter.window.focus();
        $(presenter.editor.contentDocument).find('body').focus();

        if (presenter.lastCaret) {
            presenter.caret(presenter.lastCaret);
        }
    };

    presenter.buildKeyboard = function AddonParagraph_Keyboard_buildKeyboard(){
        var keyboard = presenter.$view.find('.paragraph-keyboard'),
            row, keys, key, keyRow, $button, text, keySetLayer;
        $.each(presenter.configuration.keyboardLayout, function(set, keySet) {
            keySetLayer = $('&lt;div&gt;').addClass('keySetLayer');
            keySetLayer.addClass('keyset-' + set);

            for ( row = 0; row &lt; keySet.length; row++ ){
                keys = keySet[row];

                if (!keys) {
                    continue;
                }

                keyRow = $('&lt;div&gt;').addClass('keyRow');
                for ( key = 0; key &lt; keys.length; key++ ) {
                    // ignore empty keys
                    if (keys[key].length === 0) {
                        continue;
                    }

                    text = keys[key];
                    if (text == '{empty}') {
                        keyRow.append($('&lt;div&gt;').addClass('paragraph-keyboard-empty').html('&amp;nbsp;'));
                    } else if (text == '{shift}') {
                        $button = $('&lt;div&gt;').addClass(presenter.CSS_CLASSES.KEYBOARD_SHIFT).html('&amp;nbsp;');
                        $button.on('click', presenter.switchKeyboard);
                        keyRow.append($button);
                    } else {
                        $button = $('&lt;div&gt;');
                        $button.addClass(presenter.CSS_CLASSES.KEYBOARD_LETTER);
                        if (text.includes("\\alt")) {
                            const html = parseAltText(text);
                            $button.html(html);
                        } else {
                            $button.text(text);
                        }
                        $button.on('click', presenter.clickKeyboard);
                        $button.on('mousedown', presenter.onMouseDownKeyboard);
                        keyRow.append($button);
                    }

                    presenter.eKeyboardButtons.push($button);

                }
                keySetLayer.append(keyRow);
                keySetLayer.append($('&lt;div&gt;').addClass('keyboard-clear'));
            }
            if (set != 'default') {
                keySetLayer.hide();
            } else {
                presenter.currentKeyboard = 'default';
            }
            keyboard.append(keySetLayer);
        });
    };

    presenter.onMouseDownKeyboard = function () {
        presenter.placeholder.shouldBeSet = false;
    };

    presenter.onInit = function AddonParagraph_Keyboard_onInit() {
        presenter.window = presenter.editor.contentWindow;

        if (presenter.configuration.isToolbarHidden) {
            presenter.$view.find('.mce-container.mce-panel.mce-first').remove();
            presenter.$view.find('.' + presenter.CSS_CLASSES.MCE_EDIT_AREA).css('border-top-width', '0');
        }

        presenter.jQueryTinyMCEHTML = $(presenter.editor.dom.select('html'));
        presenter.jQueryTinyMCEHTML.click(function () {
            presenter.window.focus();
            $(presenter.editor.contentDocument).find('body').focus();
        });

        var stylesheetFullPath = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "addons/resources/style.css");
        presenter.editor.dom.loadCSS(stylesheetFullPath);

        presenter.setStyles();

        if (presenter.configuration.state !== undefined) {
        	presenter.editor.setContent(presenter.configuration.state, {format : 'raw'});
        }

        setTimeout(function () {
            if (presenter.setIframeHeight)
                presenter.setIframeHeight();
        }, 0);

        presenter.$tinyMCEToolbar = presenter.$view.find('.mce-toolbar');
        presenter.lastHeight = presenter.$tinyMCEToolbar.css('height');
        presenter.$tinyMCEToolbar.on('resize', function () {
            presenter.setIframeHeight();
        });

        checkForChanges();

        presenter.$view.find('.mce-container.mce-panel.mce-tinymce').css('border',0);

        var el = presenter.editor.dom.select('body')[0];
        presenter.$TinyMCEBody = $(el);
        if (typeof el.ownerDocument.parentWindow !== 'undefined') {
            presenter.window = el.ownerDocument.parentWindow;
            presenter.ownerDocument = el.ownerDocument;
            presenter.lastCaret = presenter.caret();
            presenter.$TinyMCEBody.on('mouseup keyup', function(e){
                presenter.lastCaret = presenter.caret();
            });
        } else {
            presenter.ownerDocument = false;
        }

        presenter.editor.on('blur', function () {
            presenter.sendOnBlurEvent();
        });

        if (presenter.configuration.isPlaceholderEditable &amp;&amp; presenter.state == null) {
            presenter.setText(presenter.configuration.placeholderText);
        }

        presenter.addEventListenerOnKeyEscapeToEditorMCE();
    };

    presenter.addEventListenerOnKeyEscapeToEditorMCE = function (){
        const mceIframe = presenter.$view.find('.' + presenter.CSS_CLASSES.MCE_EDIT_AREA)[0].childNodes[0];
        const content = (mceIframe.contentDocument || mceIframe.contentWindow.document).documentElement;
        const escapeKeyCallback = function (e) {
            if (e.keyCode === window.KeyboardControllerKeys.ESCAPE &amp;&amp; presenter.keyboardControllerObject.keyboardNavigationActive) {
                presenter.dispatchEscapeKeydownEvent();
                document.activeElement.blur();
            }
        };

        content.addEventListener("keydown", escapeKeyCallback);
    };

    presenter.dispatchEscapeKeydownEvent = function () {
        const event = new KeyboardEvent('keydown', {
            code: 'Escape',
            key: 'Escape',
            charCode: window.KeyboardControllerKeys.ESCAPE,
            keyCode: window.KeyboardControllerKeys.ESCAPE,
            bubbles: true
        });
        document.body.dispatchEvent(event);
    };

    function checkForChanges(){
        if (presenter.$tinyMCEToolbar &amp;&amp; presenter.$tinyMCEToolbar.css('height') != presenter.lastHeight){
            presenter.lastHeight = presenter.$tinyMCEToolbar.css('height');
            presenter.setIframeHeight();
            return;
        }

        checkHeightCounter += 1;
        if(checkHeightCounter == 3) return;

        setTimeout(checkForChanges, 500);
    }

    presenter.setPlayerController = function AddonParagraph_Keyboard_playerController(controller) {
        presenter.playerController = controller;

        presenter.currentPageIndex = presenter.playerController.getCurrentPageIndex();
        presenter.pageID = presenter.playerController.getPresentation().getPage(presenter.currentPageIndex).getId();
        presenter.textParser = new TextParserProxy(presenter.playerController.getTextParser());
    };

    presenter.getState = function AddonParagraph_Keyboard_getState() {
        var tinymceState = '';
        if (presenter.editor != null &amp;&amp; presenter.editor.hasOwnProperty("id")) {
            try {
                if (presenter.isShowAnswersActive) presenter.hideAnswers();
                tinymceState = presenter.editor.getContent({format : 'raw'});
            } catch (err) {
                return  presenter.state;
            }
        }

        return JSON.stringify({
            'tinymceState' : tinymceState,
            'isVisible' : presenter.isVisibleValue,
            'isLocked' : presenter.isLocked
        });
    };

    presenter.setState = function AddonParagraph_Keyboard_setState(state) {
        var parsedState = JSON.parse(state),
            tinymceState = parsedState.tinymceState;

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);

        if (tinymceState !== undefined
            &amp;&amp; tinymceState !== ""
            &amp;&amp; !isPlaceholderClassInHTML(tinymceState)) {
            if (presenter.editor != null &amp;&amp; presenter.editor.initialized) {
                presenter.editor.setContent(tinymceState, {format: 'raw'});
                presenter.state = state;
            } else {
                presenter.configuration.state = tinymceState;
                presenter.state = state;
            }
        }

        if (parsedState.isLocked) {
            presenter.lock();
        } else {
            presenter.unlock();
        }
    };

    function isPlaceholderClassInHTML (html) {
        return html.indexOf("class=\"placeholder\"") !== -1;
    }

    presenter.executeCommand = function AddonParagraph_Keyboard_executeCommand(name, params) {
        if (!presenter.configuration.isValid) { return; }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isVisible': presenter.isVisible,
            'lock': presenter.lock,
            'unlock': presenter.unlock,
            'getText': presenter.getText,
            'setText': presenter.setText,
            'isAttempted': presenter.isAttempted,
            'isAIReady': presenter.isAIReady,
            'sendUserAnswerToAICheck': presenter.sendUserAnswerToAICheck,
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.reset = function AddonParagraph_Keyboard_reset() {
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.placeholder.removePlaceholder();
        if (presenter.configuration.isPlaceholderEditable) {
            presenter.setText(presenter.configuration.placeholderText);
        } else {
            presenter.editor.setContent('');
            presenter.setStyles();
        }
        presenter.placeholder.addPlaceholder();
        if (presenter.isLocked) {
            presenter.unlock();
        }
    };

    presenter.show = function AddonParagraph_Keyboard_show() {
        presenter.setVisibility(true);
    };

    presenter.hide = function AddonParagraph_Keyboard_hide() {
        presenter.setVisibility(false);
    };

    presenter.isVisible = function AddonParagraph_Keyboard_isVisible() {
        return presenter.isVisibleValue;
    };

    presenter.lock = function AddonParagraph_Keyboard_lock() {
        if (!presenter.isLocked) {
            var mask = $('&lt;div&gt;').addClass('paragraph-lock');
            presenter.$view.find('.paragraph-keyboard').hide();
            presenter.$view.find('#' + presenter.configuration.ID + '-wrapper').append(mask);
            presenter.isLocked = true;
        }
    };

    presenter.unlock = function AddonParagraph_Keyboard_unlock() {
        if (presenter.isLocked) {
            presenter.$view.find('.paragraph-keyboard').show();
            presenter.$view.find('.paragraph-lock').remove();
            presenter.isLocked = false;
        }
    };

    presenter.getText = function AddonParagraph_Keyboard_getText() {
        return presenter.editor.getContent({format : 'raw'});
    };

    presenter.setText = function AddonParagraph_Keyboard_setText(text) {
        presenter.editor.setContent(text);
    };

    presenter.isAttempted = function AddonParagraph_Keyboard_isAttempted() {
        if (!presenter.isEditorLoaded) {
            if (presenter.state) {
                var parser = new DOMParser();
                var stateNode = parser.parseFromString(JSON.parse(presenter.state).tinymceState, "text/html");
                return $(stateNode).text() != '';
            } else {
                return false;
            }
        }
        const editorContent = presenter.getText();
        if (presenter.configuration.isPlaceholderSet
            &amp;&amp; !presenter.configuration.isPlaceholderEditable) {
            return !isPlaceholderClassInHTML(editorContent);
        }
        const textToCompare = presenter.configuration.isPlaceholderSet ? presenter.configuration.placeholderText : "";
        return $(editorContent).text() != textToCompare;
    }

    presenter.getPrintableHTML = function (model, showAnswers) {
        const upgradedModel = presenter.upgradeModel(model);
        const configuration = presenter.parseModel(upgradedModel, false);
        const modelAnswer = configuration.modelAnswer;

        const $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        $wrapper.addClass('printable_addon_Paragraph');
        $wrapper.css("left", "0px");
        $wrapper.css("right", "0px");
        $wrapper.css("height", configuration.paragraphHeight + "px");
        $wrapper.css("padding", "10px 10px 10px 0px");
        const $paragraph = $('&lt;div&gt;&lt;/div&gt;');
        $paragraph.css("left", "0px");
        $paragraph.css("right", "0px");
        $paragraph.css("height", "100%");
        $paragraph.css("border", "1px solid");

        let innerHTML = "";
        if (showAnswers) {
            innerHTML = modelAnswer;
        }
        if (presenter.printableState) {
            innerHTML = presenter.printableState;
        }
        $paragraph.html(innerHTML);

        $wrapper.append($paragraph);
        return $wrapper[0].outerHTML;
    };

    presenter.getOpenEndedContent = function () {
        return presenter.getText();
    };

    presenter.addPlugins = function AddonParagraph_Keyboard_addPlugins() {
        if (presenter.configuration.isPlaceholderSet) {
            presenter.addPlaceholderPlugin();
        }
    };

    presenter.makePluginName = function AddonParagraph_Keyboard_makePluginName(addonID) {
        var name = 'placeholder';
        addonID.replace(/[a-z0-9]+/gi, function(x) {
            name += "_" + x;
        });

        return name;
    };

    presenter.onFocus = function AddonParagraph_Keyboard_onFocus() {
        if (presenter.placeholder.isSet) {
            presenter.placeholder.removePlaceholder();
            presenter.placeholder.shouldBeSet = (presenter.placeholder.getEditorContent() == "");
        }
    };

    presenter.onBlur = function AddonParagraph_Keyboard_onBlur() {
        if (presenter.placeholder.shouldBeSet) {
            presenter.placeholder.addPlaceholder();
        } else {
            presenter.placeholder.removePlaceholder();
        }
    };

    presenter.addPlaceholderPlugin = function AddonParagraph_Keyboard_addPlaceholderPlugin() {
        tinymce.PluginManager.add(presenter.configuration.pluginName, function(editor) {
            editor.on('init', function () {
                presenter.placeholder.init(editor.id);
                editor.on('blur', presenter.onBlur);
                editor.on('focus', presenter.onFocus);
            });
        });
    };

    presenter.placeholderElement = function AddonParagraph_Keyboard_placeholderElement() {
        this.isSet = true;
        this.shouldBeSet = false;
        this.keyboardChange = false;
        this.placeholderText = presenter.configuration.isPlaceholderEditable ? "" : presenter.configuration.placeholderText;
        this.contentAreaContainer = null;
        this.el = null;
        this.attrs = {style: {position: 'absolute', top:'5px', left:0, color: '#888', padding: '1%', width:'98%', overflow: 'hidden'} };
    };

    presenter.placeholderElement.prototype.init = function AddonParagraph_Keyboard_placeholderElement_init() {
        this.contentAreaContainer = presenter.editor.getBody();
        this.el = presenter.editor.dom.add(this.contentAreaContainer, "placeholder", this.attrs, this.placeholderText);

        tinymce.DOM.setStyle(this.contentAreaContainer, 'position', 'relative');
        tinymce.DOM.addClass(this.el, "placeholder");
    };

    presenter.placeholderElement.prototype.addPlaceholder = function AddonParagraph_Keyboard_addPlaceholder() {
        this.el = presenter.editor.dom.add(this.contentAreaContainer, "placeholder", this.attrs, this.placeholderText);
        presenter.editor.dom.addClass(this.el, "placeholder");
        this.isSet = true;
        presenter.setStyles();
    };

    presenter.placeholderElement.prototype.setPlaceholderAfterEditorChange = function AddonParagraph_Keyboard_setPlaceholderAfterEditorChange() {
        if (this.getEditorContent() == "") {
            this.shouldBeSet = true;
        } else {
            this.shouldBeSet = false;
            this.removePlaceholder();
        }
    };

    presenter.placeholderElement.prototype.removePlaceholder = function AddonParagraph_Keyboard_removePlaceholder() {
        this.isSet = false;
        presenter.editor.dom.remove(this.el);
    };

    presenter.placeholderElement.prototype.getEditorContent = function AddonParagraph_Keyboard_getEditorContent() {
        return presenter.editor.getContent();
    };

    presenter.onTinymceChange = function AddonParagraph_Keyboard_onTinymceChange(editor, event) {
        this.keyboardChange = true;
        if (presenter.configuration.isPlaceholderSet) {
            presenter.placeholder.setPlaceholderAfterEditorChange();
        }
    };

    presenter.getMaxScore = function () {
        if (!presenter.configuration.isValid || !presenter.configuration.manualGrading) {
            return 0;
        }
        return presenter.configuration.weight;
    };

    presenter.isAIReady = function() {
        if (!presenter.configuration.isValid
            || !presenter.configuration.manualGrading
            || !presenter.playerController
        ) {
            return false;
        }
        return OpenActivitiesUtils.isAIReady(
            presenter.playerController,
            presenter.pageID,
            presenter.configuration.ID
        );
    };

    presenter.waitForAIGrade = function () {
        presenter.startTime = new Date().getTime();
        presenter.disableEdit();
        presenter.aiGradeInterval = setInterval(function () {
            presenter.checkTimer();
        }, 1000);
    };

    presenter.checkTimer = function () {
        const currentTime = new Date().getTime();
        if ((currentTime - presenter.startTime) / 1_000 &gt;= presenter.MAX_WAIT_TIME) {
            clearInterval(presenter.aiGradeInterval);
            presenter.isWaitingForAIGrade = false;
            presenter.enableEdit();
        }
    };

    presenter.handleUpdateScoreEvent = function (eventData) {
        if (eventData.source !== presenter.configuration.ID &amp;&amp; eventData.value !== presenter.updateScoreEventName) { return; }

        clearInterval(presenter.aiGradeInterval);
        presenter.isWaitingForAIGrade = false;
        presenter.enableEdit();
    };

    presenter.isOpenActivity = function () {
        return presenter.configuration.isValid &amp;&amp; presenter.configuration.manualGrading;
    };

    function ParagraphKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    ParagraphKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    ParagraphKeyboardController.prototype.constructor = ParagraphKeyboardController;

    presenter.buildKeyboardController = function Paragraph_buildKeyboardController () {
        presenter.keyboardControllerObject = new ParagraphKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
        presenter.keyboardControllerObject.overrideKeysHandlers();
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return this.$view.find(`.${presenter.CSS_CLASSES.MCE_BUTTON}, .${presenter.CSS_CLASSES.MCE_EDIT_AREA}, .${presenter.CSS_CLASSES.KEYBOARD_LETTER}, .${presenter.CSS_CLASSES.KEYBOARD_SHIFT}`);
    };

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    ParagraphKeyboardController.prototype.overrideKeysHandlers = function () {
        this.mapping[window.KeyboardControllerKeys.ARROW_UP] = this.previousElement;
        this.mapping[window.KeyboardControllerKeys.ARROW_DOWN] = this.nextElement;
    };

    ParagraphKeyboardController.prototype.selectAction = function () {
        if (presenter.isShowAnswersActive
            || presenter.isGradualShowAnswersActive
            || presenter.isErrorCheckingMode) {
            return;
        }

        const $element = this.getTarget(this.keyboardNavigationCurrentElement, true);
        if ($element.hasClass(presenter.CSS_CLASSES.MCE_EDIT_AREA)) {
            presenter.editor.execCommand('mceCodeEditor');
        } else if ($element.hasClass(presenter.CSS_CLASSES.MCE_BUTTON)) {
            $element[0].click();
            document.activeElement.blur();
            presenter.speakSelectedOnAction($element);
        } else if ($element.hasClass(presenter.CSS_CLASSES.KEYBOARD_LETTER)) {
            $element[0].click();
            document.activeElement.blur();
            $element[0].focus();
            presenter.speakSelectedOnAction($element);
        } else if ($element.hasClass(presenter.CSS_CLASSES.KEYBOARD_SHIFT)) {
            $element[0].click();
            document.activeElement.blur();
            presenter.speakSelectedOnAction($element);
            this.firstVisibleShift();
        }
        this.mark(this.keyboardNavigationCurrentElement);
    };

    presenter.speakSelectedOnAction = function ($element) {
        const textToSpeak = (
            $element.hasClass(presenter.CSS_CLASSES.MCE_ACTIVE)
            || $element.hasClass(presenter.CSS_CLASSES.KEYBOARD_LETTER)
            || $element.hasClass(presenter.CSS_CLASSES.KEYBOARD_SHIFT)
        )
            ? presenter.speechTexts.selected
            : presenter.speechTexts.deselected;
        presenter.speak(textToSpeak);
    };

    ParagraphKeyboardController.prototype.mark = function (element) {
        const $target = this.getTarget(element, false);
        $target.addClass('keyboard_navigation_active_element_important');
        if ($target.hasClass(presenter.CSS_CLASSES.MCE_EDIT_AREA)) {
            $target.addClass('keyboard-navigation-margin');
        }
    };

    ParagraphKeyboardController.prototype.unmark = function (element) {
        const $target = this.getTarget(element, false);
        $target.removeClass('keyboard_navigation_active_element_important');
        if ($target.hasClass(presenter.CSS_CLASSES.MCE_EDIT_AREA)) {
            $target.removeClass('keyboard-navigation-margin');
        }
    };

    ParagraphKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
    };

    ParagraphKeyboardController.prototype.nextElement = function (event) {
        if (event) {
            event.preventDefault();
        }

        const wasSuccess = this.nextVisibleElement();
        if (wasSuccess) {
            this.readCurrentElement();
        }
    };

    ParagraphKeyboardController.prototype.previousElement = function (event) {
        if (event) {
            event.preventDefault();
        }

        const wasSuccess = this.previousVisibleElement();
        if (wasSuccess) {
            this.readCurrentElement();
        }
    };

    /**
     * Finds and switches to the next visible element in the keyboard navigation elements list.
     * @method nextVisibleElement
     * @returns {boolean} - Returns `true` if a visible element is found and switched to,
     *                      otherwise returns `false` if no visible elements are found.
     */
    ParagraphKeyboardController.prototype.nextVisibleElement = function () {
        for (var i = 1; i &lt; this.keyboardNavigationElementsLen; i++) {
            var index = (this.keyboardNavigationCurrentElementIndex + i) % this.keyboardNavigationElementsLen;
            var element = this.keyboardNavigationElements[index];
            if (!this.isElementHidden(element)) {
                this.markCurrentElement(index);
                return true;
            }
        }
        return false;
    };

    /**
     * Finds and switches to the previous visible element in the keyboard navigation elements list.
     * @method previousVisibleElement
     * @returns {boolean} - Returns `true` if a visible element is found and switched to,
     *                      otherwise returns `false` if no visible elements are found.
     */
    ParagraphKeyboardController.prototype.previousVisibleElement = function () {
        for (let i = 1; i &lt; this.keyboardNavigationElementsLen; i++) {
            const index = (this.keyboardNavigationCurrentElementIndex - i + this.keyboardNavigationElementsLen) % this.keyboardNavigationElementsLen;
            const element = this.keyboardNavigationElements[index];
            if (!this.isElementHidden(element)) {
                this.markCurrentElement(index);
                return true;
            }
        }
        return false;
    };

    ParagraphKeyboardController.prototype.firstVisibleShift = function () {
        for (let i = 1; i &lt; this.keyboardNavigationElementsLen; i++) {
            const index = (this.keyboardNavigationCurrentElementIndex - i + this.keyboardNavigationElementsLen) % this.keyboardNavigationElementsLen;
            const element = this.keyboardNavigationElements[index];
            if (!this.isElementHidden(element) &amp;&amp; $(element).hasClass(presenter.CSS_CLASSES.KEYBOARD_SHIFT)) {
                this.markCurrentElement(index);
                element.focus();
                return true;
            }
        }
        return false;
    };

    /**
     * Checks if a given DOM element is hidden.
     *
     * An element is considered hidden if:
     * - Its height or width is 0.
     * - Its CSS `display` property is set to `none`.
     * - Its CSS `visibility` property is not `visible`.
     *
     * @param {HTMLElement} element - The DOM element to check.
     * @returns {boolean} - Returns `true` if the element is hidden, otherwise `false`.
     */
    ParagraphKeyboardController.prototype.isElementHidden = function (element) {
        const elementHeight = element.offsetHeight;
        const elementWidth = element.offsetWidth;
        const isDisplayed = $(element).css('display') !== 'none';
        const isVisible = $(element).css('visibility') === 'visible';

        return elementHeight === 0 || elementWidth === 0 || !isDisplayed || !isVisible;
    };

    ParagraphKeyboardController.prototype.getCurrentElement = function () {
		return this.getTarget(this.keyboardNavigationCurrentElement, false);
	};

    ParagraphKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    ParagraphKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    ParagraphKeyboardController.prototype.readCurrentElement = function () {
        const $element = this.getCurrentElement();
        const ariaLabel = $element.attr("aria-label");
        let text = "";

        if ($element.hasClass(presenter.CSS_CLASSES.KEYBOARD_LETTER)) {
            text = TTSUtils.getTextVoiceArrayFromElement($element, presenter.configuration.langTag);
        } else if ($element.hasClass(presenter.CSS_CLASSES.KEYBOARD_SHIFT)) {
            text = presenter.speechTexts.shift;
        } else if ($element.hasClass(presenter.CSS_CLASSES.MCE_BUTTON) &amp;&amp; ariaLabel) {
            const label = ariaLabel.toLowerCase().replace(/\s/gm, "");
            const ttsKey = presenter.TOOLBAR_ARIAS[label];
            text = ttsKey ? presenter.speechTexts[ttsKey] : ariaLabel;
            if ($element.hasClass(presenter.CSS_CLASSES.MCE_ACTIVE)) {
                text += ` ${presenter.speechTexts.selected}`;
            }
        } else if ($element.hasClass(presenter.CSS_CLASSES.MCE_EDIT_AREA)) {
            let $contentToRead = $(presenter.editor.getContent({format : 'raw'}));
            if ($contentToRead.text().trim().length === 0) {
                text = presenter.speechTexts.paragraphContent;
            } else {
                text = TTSUtils.getTextVoiceArrayFromElement($contentToRead, presenter.configuration.langTag);
            }
        } else {
            let content;
            try {
                content = $element[0].textContent;
            } catch (error) {
                console.error(error);
                content = "element";
            }
            text = content;
        }

        presenter.speak(text);
    };

    presenter.speak = function (data) {
        const tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function Paragraph_setWCAGStatus(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function Paragraph_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function parseAltText(text) {
        return presenter.isPreview ? window.TTSUtils.parsePreviewAltText(text) : presenter.textParser.parseAltTexts(text);
    }

    return presenter;
}

AddonParagraph_Keyboard_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="PieChart" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="Items" nameLabel="PieChart_property_items" type="list">
            <property name="Name" nameLabel="PieChart_property_name" type="string"/>
            <property name="Color" nameLabel="PieChart_property_color" type="string"/>
            <property name="Starting percent" nameLabel="PieChart_property_starting_percent" type="string"/>
            <property name="Answer" nameLabel="PieChart_property_answer" type="string"/>
        </property>
        <property name="Step" nameLabel="PieChart_property_step" type="string"/>
        <property name="Show values" nameLabel="PieChart_property_show_values" type="boolean"/>
        <property name="Show names" nameLabel="PieChart_property_show_names" type="boolean"/>
        <property name="Is Activity" nameLabel="PieChart_property_is_activity" type="boolean"/>
        <property name="Is Disabled" nameLabel="PieChart_property_is_disabled" type="boolean"/>
        <property displayName="Radius size" name="Radius" nameLabel="PieChart_property_radius" type="string"/>
		<property displayName="Percents positions" name="Percents" nameLabel="PieChart_property_percents" type="string"/>
    </model>
<css>.addon_PieChart{
    -ms-touch-action: none;
}
.piechart {
    border: 3px solid #ccc;
    float: left;
}
.disabled {
    opacity: 0.5;
}
.correct {
    border: 3px solid green;
}
.wrong {
    border: 3px solid red;
}
.graph {
    stroke: #000;
    stroke-width: 2px;
    fill-opacity: 0;
}
.line {
    stroke-width: 20px;
    stroke: #A52A2A;
    stroke-opacity: 0;
}
.item {
    fill: red;
    background: red;
}
.item1 {
    fill: #ffaaff;
    background: #ffaaff;
}
.item2 {
    fill: #ffaaaa;
    background: #ffaaaa;
}
.item3 {
    fill: #00aaaa;
    background: #00aaaa;
}
.legend {
    border:1px solid #000;
    padding: 5px;
    float: left;
    margin: 10px;
}
.legendItem {
    padding-bottom:2px;
}
.legendSquare {
    width: 25px;
    height: 25px;
    margin-right: 10px;
    display: inline-block;
}
.legendText {
    display: inline;
}
.percentsValues{
    fill: blue;
}
</css><view>&lt;div class="piechart"&gt;&lt;/div&gt;
</view><preview>&lt;div class="piechart"&gt;&lt;/div&gt;
</preview><presenter>function AddonPieChart_create(){
    var presenter = function(){};
    presenter.error = false;
    var i, ii, j, tmp1, tmp2, tmp3, tmp4;
    presenter.move = false;
    presenter.isMoved = false;
    presenter.isLineInMove = false;
    presenter.isErrorCheckingMode = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;

    presenter.ERROR_CODES = {
        'WrongStep' : 'The step is wrong!',
        'WrongStart' : 'Wrong starting data!',
        'WrongAnswer' : 'Wrong answers!',
        'WrongName' : 'Define the names!',
        'WrongColor' : 'Wrong color!',
        'WrongSize' : 'Wrong Radius size, choose a number beetween 0 and 1!',
        'WrongPosition' : 'Wrong Percents positions, choose a number beetween 0 and 1!'
    };

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'isAllOK'.toLowerCase():
                presenter.isAllOK();
                break;
            case 'isOK'.toLowerCase():
                presenter.isOK(params[0]);
                break;
            case 'getPercent'.toLowerCase():
                presenter.getPercent(params[0]);
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
        }
    };

    presenter.onEventReceived = function (eventName, eventData) {
         switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.isAttempted = function() {
        presenter.handleDisplayedAnswers();
        return !presenter.activity || presenter.isMoved;
    };

    presenter.isAllOK = function() {
        presenter.handleDisplayedAnswers();
        return presenter.getScore() === presenter.getMaxScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.isOK = function(item) {
        presenter.handleDisplayedAnswers();
        return presenter.activity &amp;&amp; presenter.currentPercents[item-1] == presenter.items[item-1]["Answer"];
    };

    presenter.getPercent = function(item) {
        presenter.handleDisplayedAnswers();
        return presenter.currentPercents[item-1];
    };

    presenter.hide = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        (presenter.isVisible) ?	presenter.show() : presenter.hide();
    };

    presenter.disable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.disabled = true;
        presenter.$view.find('.piechart').addClass('disabled');
    };

    presenter.enable = function() {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.disabled = false;
        presenter.$view.find('.piechart').removeClass('disabled');
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(score) {
        return {
            source : presenter.addonID,
            item : '',
            value : '',
            score : score
        };
    };

    presenter.triggerLineEvent = function(line, state, score) {
        var eventData = presenter.createEventData(line, state, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.validateColor = function(colour){
        if (colour === '#28e32a') {
            return true;
        }
        var rgb = $('&lt;div style="color:#28e32a"&gt;');
        var valid_rgb = "rgb(40, 227, 42)";
        rgb.css("color", colour);
        if(rgb.css('color') == valid_rgb &amp;&amp; colour != ':#28e32a' &amp;&amp; colour.replace(/ /g,"") != valid_rgb.replace(/ /g,"") &amp;&amp; colour !== '') {
            return false;
        } else {
            return true;
        }
    };

    presenter.initiate = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        presenter.piechart = presenter.$view.find('.piechart');
        presenter.activity = ModelValidationUtils.validateBoolean(presenter.model['Is Activity']);
        presenter.disabled = ModelValidationUtils.validateBoolean(presenter.model['Is Disabled']);
        presenter.initDisabled = presenter.disabled;
        if (presenter.disabled) presenter.disable();
        presenter.isVisible = ModelValidationUtils.validateBoolean(presenter.model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        presenter.values = ModelValidationUtils.validateBoolean(presenter.model['Show values']);
        presenter.names = ModelValidationUtils.validateBoolean(presenter.model['Show names']);
        presenter.step = presenter.model['Step'];
        presenter.items = presenter.model['Items'];
        presenter.radiusSize = presenter.model['Radius'];
        presenter.percentsPosition = presenter.model['Percents'];
        presenter.numberOfItems = presenter.items.length;
        tmp1 = 0;
        tmp2 = 0;
        if (presenter.radiusSize === '' &amp;&amp; presenter.values) {
            presenter.radiusSize = 0.7;
        } else if (presenter.radiusSize === '') {
            presenter.radiusSize = 1;
        } else if (isNaN(presenter.radiusSize) || presenter.radiusSize &lt;= 0 ||	presenter.radiusSize &gt; 1) {
            presenter.error = 'WrongSize';
            return false;
        }
        if (presenter.percentsPosition == '') {
            presenter.percentsPosition = 0.85;
        } else if (isNaN(presenter.percentsPosition) || presenter.percentsPosition &lt;= 0 ||	presenter.percentsPosition &gt; 1) {
            presenter.error = 'WrongPosition';
            return false;
        }
        (presenter.step == '') ? (presenter.step = parseFloat(1)) : (presenter.step = parseFloat(presenter.step));
        if (presenter.step &lt; 0 || presenter.step &gt;=100 || isNaN(presenter.step)) {
            presenter.error = 'WrongStep';
            return false;
        }
        presenter.startingLines = new Array(presenter.numberOfItems);
        presenter.angles = new Array(presenter.numberOfItems);
        for (i = 0; i &lt; presenter.numberOfItems; i++) {
            if (!presenter.validateColor(presenter.items[i]['Color'])) {
                presenter.error = 'WrongColor';
                return false;
            }
            if (isNaN(presenter.items[i]['Starting percent']) || presenter.items[i]['Starting percent'] === '') {
                presenter.error = 'WrongStart';
                return false;
            }
            if (isNaN(presenter.items[i]['Answer']) || (presenter.activity &amp;&amp; presenter.items[i]['Starting percent'] === '')) {
                presenter.error = 'WrongAnswer';
                return false;
            }
            if (presenter.names &amp;&amp; presenter.items[i]['Name'] === '') {
                presenter.error = 'WrongName';
                return false;
            }
            tmp1 += parseFloat(presenter.items[i]['Starting percent']);
            tmp2 += parseFloat(presenter.items[i]['Answer']);
            if((Math.abs(presenter.items[i]['Starting percent'] - presenter.items[i]['Answer']) % presenter.step) !== 0 &amp;&amp; presenter.activity) {
                presenter.error = 'WrongStep';
                return false;
            }
        }
        if (tmp1 !== 100) {
            presenter.error = 'WrongStart';
            return false;
        }
        if (tmp2 !== 100 &amp;&amp; presenter.activity) {
            presenter.error = 'WrongAnswer';
            return false;
        }
        return true;
    };

    presenter.drawGraph = function(type) {
        var wrapper = presenter.piechart.parent();
        var graphSize = Math.min(wrapper.width(),wrapper.height());
        presenter.center = Math.floor(graphSize / 2);
        presenter.radius = presenter.center * presenter.radiusSize-5;
        presenter.piechart.css({'width' : graphSize, 'height' : graphSize});
        var x1, x2, y1, y2, x3, y3, angle, angle2, lines, percents;
        var $svg = '&lt;svg height="'+graphSize+'" width="'+graphSize+'" version="1.1" xmlns:xlink="http://www.w3.org/2000/svg" class="chart'
        if (type == 'showAnswers') $svg += '-show-answers';
        $svg += '"&gt;';
        angle = 0;
        lines = '';
        percents = '';
        var parameter;
        if (type != 'showAnswers') {
            presenter.currentPercents = new Array(presenter.numberOfItems);
            presenter.startingItems = new Array(presenter.numberOfItems);
        }
        for (i = 0; i &lt; presenter.numberOfItems; i++) {
            if (type == 'showAnswers') {
                parameter = parseFloat(presenter.items[i]['Answer']);
            } else {
                parameter = parseFloat(presenter.items[i]['Starting percent']);
            }
            x1 = parseFloat(presenter.center + presenter.radius * Math.sin(angle));
            y1 = parseFloat(presenter.center - presenter.radius * Math.cos(angle));
            angle2 = angle + 0.5*(parameter)/50 * Math.PI;
            angle += (parameter)/50 * Math.PI; //if this changes, fix generateItemData
            x2 = parseFloat(presenter.center + presenter.radius * Math.sin(angle));
            y2 = parseFloat(presenter.center - presenter.radius * Math.cos(angle));
            x3 = parseFloat(presenter.center + Math.sin(angle2)*(presenter.center * presenter.percentsPosition));
            y3 = parseFloat(presenter.center - Math.cos(angle2)*(presenter.center * presenter.percentsPosition));
            $svg += '&lt;path id="item'+(i+1)+'" class="item item'+(i+1)+'" d="';
            ItemData = 'M '+presenter.center+' '+presenter.center+' L '+x1+' '+y1+' A '+presenter.radius+' '+presenter.radius+' 0 ';
            ((parameter) &gt; 50) ? (ItemData += '1') : (ItemData += '0');
            ItemData += ' 1 '+x2+' '+y2+' L '+presenter.center+' '+presenter.center+' Z';
            if (type != 'showAnswers') presenter.startingItems[i] = ItemData;
            $svg += ItemData + '" stroke-width="0"';
            if (presenter.items[i]['Color'] !== '') {
                $svg += 'style="fill: '+presenter.items[i]['Color']+';"';
            }
            $svg += '&gt;&lt;/path&gt;';
            if (type != 'showAnswers')
                presenter.currentPercents[i] = (parameter);
            if (presenter.values) {
                percents += presenter.drawPercent(i,x3,y3,parameter);
            }
            if (type != 'showAnswers') {
                presenter.angles[i] = ((angle/Math.PI*180-180)+360)%360;  //if this changes, fix generateItemData
                presenter.startingLines[i] = presenter.angles[i];
            }
            lines += '&lt;rect id="'+(i+1)+'" class ="line" height="'+presenter.radius+'" width="2" y="'+presenter.center+'" x="'+(presenter.center-1)+'" transform="rotate('+ (((angle/Math.PI*180-180)+360)%360) +', '+presenter.center+', '+presenter.center+')"&gt;&lt;/rect&gt;';
        }
        $svg += '&lt;circle class="graph';
        $svg += '" r="'+presenter.radius+'" cy="'+presenter.center+'" cx="'+presenter.center+'"&gt;&lt;/circle&gt;';
        $svg += lines;
        $svg += percents;
        $svg += '&lt;/svg&gt;';
        presenter.piechart.prepend($svg);
    };

    presenter.generateItemData = function(parameter, angleDegrees) {
        // the method is based on drawGraph
        let angle = angleDegrees * Math.PI / 180 + Math.PI  - (parameter)/50 * Math.PI;
        angle = (angle + 2*Math.PI) % (2 * Math.PI);
        let x1 = parseFloat(presenter.center + presenter.radius * Math.sin(angle));
        let y1 = parseFloat(presenter.center - presenter.radius * Math.cos(angle));
        angle += (parameter)/50 * Math.PI;
        let x2 = parseFloat(presenter.center + presenter.radius * Math.sin(angle));
        let y2 = parseFloat(presenter.center - presenter.radius * Math.cos(angle));
        let ItemData = 'M '+presenter.center+' '+presenter.center+' L '+x1+' '+y1+' A '+presenter.radius+' '+presenter.radius+' 0 ';
        ((parameter) &gt; 50) ? (ItemData += '1') : (ItemData += '0');
        ItemData += ' 1 '+x2+' '+y2+' L '+presenter.center+' '+presenter.center+' Z';
        return ItemData;
    }

    presenter.drawPercent = function(i,x,y,value) {
        var tmp = '&lt;text id="Text'+(i+1)+'" class="percentsValues" x="'+x+'" y="'+y+'" text-anchor="middle"&gt;'+value+'%&lt;/text&gt;';
        return tmp;
    };

    presenter.drawLegend = function() {
        var $legend = '&lt;div class = "legend"&gt;', colorItem = '';
        for (i = 0; i &lt; presenter.numberOfItems; i++) {
            if (presenter.items[i]['Color'] !== '') {
                colorItem = 'style="background: '+presenter.items[i]['Color']+';"';
            }
            $legend += '&lt;div class="legendItem"&gt;&lt;div class="legendSquare item'+(i+1)+' item"'+colorItem+'&gt;&lt;/div&gt;&lt;div class="legendText"&gt;'+ presenter.items[i]['Name']+'&lt;/div&gt;&lt;/div&gt;';
            colorItem = '';
        }
        $legend += '&lt;/div';
        presenter.$view.find('.piechart').parent().append($legend);
    };

    presenter.run = function(view, model){
        var x, y, angle, k, angle2, percent, angleTmp, previousItem, nextItem;
        if (!presenter.initiate(view, model)) {
            presenter.piechart.text(presenter.ERROR_CODES[presenter.error]);
            return false;
        }
        presenter.updateVisibility();
        var Width = presenter.piechart.width();
        var Height = presenter.piechart.height();
        presenter.drawGraph();
        if (presenter.names) {
            presenter.drawLegend();
        }
        presenter.$view.find('.line')
            .mousedown(function(e) {
                e.stopImmediatePropagation();
                e.preventDefault();
                i = parseInt($(this).attr('id'),10);
                e.stopPropagation();
                presenter.isLineInMove = true;
            });
        presenter.$view
            .click(function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (presenter.isLineInMove &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    (presenter.isAllOK()) ? (score=1) : (score=0);
                    if (!presenter.activity) score ='';
                    presenter.triggerLineEvent(score);
                }
                presenter.isLineInMove = false;
            })
            .mouseup(function(e) {
                e.stopPropagation();
                e.preventDefault();
                if (presenter.isLineInMove &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    (presenter.isAllOK()) ? (score=1) : (score=0);
                    if (!presenter.activity) score ='';
                    presenter.triggerLineEvent(score);
                }
                presenter.isLineInMove = false;
            })
            .mouseleave(function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (presenter.isLineInMove &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    (presenter.isAllOK()) ? (score=1) : (score=0);
                    if (!presenter.activity) score ='';
                    presenter.triggerLineEvent(score);
                }
                presenter.isLineInMove = false;
            })
            .mousemove(function(e) {
                e.preventDefault();
                e.stopPropagation();
                if (presenter.isLineInMove 	&amp;&amp; !presenter.disabled &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.isShowAnswersActive) {
                    presenter.isMoved = true;
                    x = e.originalEvent.pageX - presenter.piechart.offset().left - presenter.center;
                    y = presenter.piechart.offset().top - e.originalEvent.pageY + presenter.center;
                    //	alert("x: "+x+"   y:"+y);
                    for (k = 1; k &lt;= presenter.numberOfItems; k++) {
                        if (presenter.currentPercents[k-1] === 100) {
                            i = k;
                        }
                    }
                    presenter.doTheMove(i,x,y);
                }
            });
        presenter.$view.find('.line').on('touchstart', function(e){
            e.stopPropagation();
            e.preventDefault();
            i = parseInt($(this).attr('id'),10);
            presenter.isLineInMove = true;
        });
        presenter.$view.find('.piechart').on('touchend', function(e){
            e.stopPropagation();
            e.preventDefault();
            if (presenter.isLineInMove &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                (presenter.isAllOK()) ? (score=1) : (score=0);
                if (!presenter.activity) score ='';
                presenter.triggerLineEvent(score);
            }
            presenter.isLineInMove = false;
        });
        presenter.$view.find('.piechart').on('touchmove', function (e) {
            var scale = presenter.playerController.getScaleInformation();
            var center_scaled = presenter.center * scale.scaleX;
            e.stopPropagation();
            e.preventDefault();
            x = (parseInt(e.originalEvent.touches[0].pageX, 10) / scale.scaleX - presenter.piechart.offset().left / scale.scaleX - presenter.center);
            y = (presenter.piechart.offset().top / scale.scaleY - parseInt(e.originalEvent.touches[0].pageY, 10) / scale.scaleY + presenter.center);
            if (presenter.isLineInMove &amp;&amp; (x &lt; -center_scaled || y &lt; -center_scaled || x &gt; center_scaled || y &gt; center_scaled) &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.isShowAnswersActive) {
                presenter.isLineInMove = false;
                (presenter.isAllOK()) ? (score=1) : (score=0);
                if (!presenter.activity) score ='';
                presenter.triggerLineEvent(score);
            }
            if (presenter.isLineInMove 	&amp;&amp; !presenter.disabled &amp;&amp; !presenter.isErrorCheckingMode &amp;&amp; !presenter.isShowAnswersActive) {
                presenter.isMoved = true;
                for (k = 1; k &lt;= presenter.numberOfItems; k++) {
                    if (presenter.currentPercents[k-1] === 100) {
                        i = k;
                    }
                }
                presenter.doTheMove(i,x,y);
            }
        });
        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        events.forEach((eventName) =&gt; {
            presenter.eventBus.addEventListener(eventName, this);
        });
    };

    presenter.doTheMove = function(i,x,y) {
        var scale = presenter.playerController.getScaleInformation();
        var angle, angle2, angleTmp, previousItem, nextItem, smallerAngle, isAcute, greaterAngle, isAcTwo;

        if (i === presenter.numberOfItems) {
            nextItem = 1;
        } else {
            nextItem = i+1;
        }
        if (i === 1) {
            previousItem = presenter.numberOfItems;
        } else {
            previousItem = i-1;
        }
        angle = Math.atan((x)/(y))*180/Math.PI;
        if (y &gt;= 0) {
            angle = 180 + angle;
        }
        presenter.move = false;
        angle = (angle + 360) % 360;
        smallerAngle = (presenter.angles[i-1] + presenter.step * (1/scale.scaleX)/100*360 + 1)%360 - 1 ;
        isAcute = (angle - presenter.angles[i-1] +360)%360;
        greaterAngle = (presenter.angles[i-1] - presenter.step * (1/scale.scaleX)/100*360 + 359) % 360+1 ;
        isAcTwo = (presenter.angles[i-1] -angle + 360)%360;
        if (isAcute &lt; 90 &amp;&amp; ((angle &gt;= smallerAngle ) || angle &lt;= greaterAngle)){
            angle = presenter.angles[i-1] + presenter.step/100*360;
            presenter.move = 'plus';
        } else if ((isAcTwo &lt; 90) &amp;&amp; ((angle &gt;= smallerAngle ) || angle &lt;= greaterAngle)){
            angle = presenter.angles[i-1] - presenter.step/100*360;
            presenter.move = 'minus';
        }
        angle = (angle + 360) % 360;
        if ((presenter.move == 'plus' &amp;&amp; presenter.currentPercents[nextItem-1]-presenter.step &lt; 0) || (presenter.move == 'minus' &amp;&amp; presenter.currentPercents[i-1]-presenter.step &lt; 0)) {
            presenter.move = false;
        }
        if (presenter.move !== false) {
            angle = (angle + 360) % 360;
            presenter.$view.find('#'+i).attr("transform", "rotate(" + angle + ", "+presenter.center+", "+presenter.center+")");
            presenter.angles[i-1] = angle;
            angle = (angle + 180) /180 * Math.PI;
            x1 = parseFloat(presenter.center + presenter.radius * Math.sin(angle));
            y1 = parseFloat(presenter.center - presenter.radius * Math.cos(angle));
            tmp1 = presenter.$view.find('#item'+(i)).attr('d');
            tmp4 = " 1 "+x1+" "+y1+" L";
            tmp3 = tmp1.replace(/ 1 \d+\.?\d* \d+\.?\d* L/, tmp4);
            tmp1 = presenter.$view.find('#item'+(nextItem)).attr('d');
            tmp2 = tmp1.replace(/L \d+\.?\d* \d+\.?\d* A/, "L "+x1+" "+y1+" A");
            angle2 = (Math.round((presenter.angles[i-1] - presenter.angles[previousItem-1])*100)/100+360)%360;
            if (angle2 &gt; 180 || (angle2 &lt;0 &amp;&amp; angle2 &gt; -180)) {
                tmp3 = tmp3.replace(" 0 0 1 "," 0 1 1 ");
            } else {
                tmp3 = tmp3.replace(" 0 1 1 "," 0 0 1 ");
            }
            angleTmp = presenter.angles[previousItem-1] + 0.5*angle2-180;
            angle2 = (Math.round((presenter.angles[nextItem-1] - presenter.angles[i-1])*100)/100+360)%360;
            if (angle2 &gt; 180 || (angle2 &lt;0 &amp;&amp; angle2 &gt; -180)) {
                tmp2 = tmp2.replace(" 0 0 1 "," 0 1 1 ");
            } else {
                tmp2 = tmp2.replace(" 0 1 1 "," 0 0 1 ");
            }
            if (presenter.move == 'plus') {
                presenter.currentPercents[i-1]+=presenter.step;
                presenter.currentPercents[nextItem-1]-=presenter.step;
            } else {
                presenter.currentPercents[i-1]-=presenter.step;
                presenter.currentPercents[nextItem-1]+=presenter.step;
            }
            presenter.$view.find('#item'+(i)).attr("d", tmp3);
            angle = 0;
            if (presenter.currentPercents[i-1] === 100) {
                if (presenter.items[i-1]['Color'] != '') {
                    presenter.$view.find('.graph').attr("style", "fill:"+presenter.items[i-1]['Color']+"; fill-opacity:1;");
                } else {
                    presenter.$view.find('.graph').attr("class","graph item item"+i);
                    presenter.$view.find('.graph').attr("style", "fill-opacity:1");
                }
                angleTmp += 180;
            } else if (presenter.currentPercents[nextItem-1] === 100) {
                if (presenter.items[nextItem-1]['Color'] != '') {
                    presenter.$view.find('.graph').attr("style", "fill:"+presenter.items[nextItem-1]['Color']+"; fill-opacity:1;");
                } else {
                    presenter.$view.find('.graph').attr("class","graph item item"+nextItem);
                    presenter.$view.find('.graph').attr("style", "fill-opacity:1");
                }
                angle = 180;
            } else {
                presenter.$view.find('.graph').attr("style", "fill-opacity:0");
                presenter.$view.find('.graph').attr("class","graph");
            }
            presenter.$view.find('#item'+(nextItem)).attr("d", tmp2);
            presenter.changePercent(i, angleTmp);
            angleTmp = presenter.angles[i-1] + 0.5*angle2-180 + angle;
            presenter.changePercent(nextItem, angleTmp);
        }
    };

    presenter.changePercent = function(id, angle) {
        presenter.$view.find('#Text'+id).attr("x", (parseFloat(presenter.center + Math.sin(angle/180 * Math.PI)*(presenter.center * presenter.percentsPosition))));
        presenter.$view.find('#Text'+id).attr("y", (parseFloat(presenter.center - Math.cos(angle/180 * Math.PI)*(presenter.center * presenter.percentsPosition))));
        presenter.$view.find('#Text'+id)[0].textContent = presenter.currentPercents[id-1]+'%';
    };

    presenter.createPreview = function(view, model) {
        if (!presenter.initiate(view, model)) {
            presenter.piechart.text(presenter.ERROR_CODES[presenter.error]);
            return false;
        }
        presenter.drawGraph();
        if (presenter.names) {
            presenter.currentPercents = new Array(presenter.numberOfItems);
            for (i = 0; i &lt; presenter.numberOfItems; i++) {
                presenter.currentPercents[i] = parseFloat(presenter.items[i]['Starting percent']);
            }
            presenter.drawLegend();
        }
    };

    presenter.setShowErrorsMode = function(){
        presenter.handleDisplayedAnswers();
        presenter.isErrorCheckingMode = true;
        if (!presenter.activity)
            return 0;
        if (presenter.getErrorCount() &gt; 0) {
            presenter.$view.find('.piechart').addClass('wrong');
        } else if (presenter.getScore() &gt; 0) {
            presenter.$view.find('.piechart').addClass('correct');
        }
    };

    presenter.setWorkMode = function(){
        presenter.isErrorCheckingMode = false;
        presenter.$view.find('.piechart').removeClass('wrong');
        presenter.$view.find('.piechart').removeClass('correct');
    };

    presenter.reset = function(){
        presenter.handleDisplayedAnswers();
        var angle, angle2, angleTmp, x1, y1;
        presenter.isMoved = false;
        if (!presenter.error) {
            presenter.disabled = presenter.initDisabled;
            presenter.disabled ? presenter.disable() : presenter.enable();
            presenter.isVisible = presenter.initIsVisible;
            presenter.updateVisibility();
            for (i = 0; i &lt; presenter.numberOfItems; i++) {
                ii = (i===0) ? (presenter.numberOfItems-1) : i-1;
                presenter.$view.find('#'+(i+1)).attr("transform", "rotate(" + presenter.startingLines[i] + ", "+presenter.center+", "+presenter.center+")");
                presenter.$view.find('#item'+(i+1)).attr("d", presenter.startingItems[i]);
                angle = (Math.round((presenter.startingLines[i] - presenter.startingLines[ii])*100)/100+360)%360;
                angleTmp = presenter.startingLines[ii] + 0.5*angle + 180;
                presenter.angles[i] = presenter.startingLines[i];
                presenter.currentPercents[i] = parseFloat(presenter.items[i]['Starting percent']);
                if (parseFloat(presenter.items[i]['Starting percent']) === 100) {
                    if (presenter.items[i]['Color'] != '') {
                        presenter.$view.find('.graph').attr("style", "fill:"+presenter.items[i]['Color']+"; fill-opacity:1;");
                    } else {
                        presenter.$view.find('.graph').attr("class","graph item item"+(i+1));
                        presenter.$view.find('.graph').attr("style", "fill-opacity:1");
                    }
                    angleTmp += 180;
                }
                if (presenter.values) presenter.changePercent((i+1), angleTmp);
            }
        }
        presenter.setWorkMode();
    };

    presenter.getErrorCount = function(){
        if (presenter.error || !presenter.activity || !presenter.isMoved) {
            return 0;
        }
        return executeWithoutShownAnswers(_getErrorCount);
    };

    function _getErrorCount() {
        let errorCount = 0;
        for (let i = 0; i &lt; presenter.numberOfItems; i++) {
            if (!(presenter.currentPercents[i] == presenter.items[i]["Answer"])) {
                errorCount = 1;
                break;
            }
        }
        return errorCount;
    }

    presenter.getMaxScore = function(){
        return (presenter.error || !presenter.activity) ? 0 : 1;
    };

    presenter.getScore = function(){
        if (presenter.error || !presenter.activity) {
            return 0;
        }
        return executeWithoutShownAnswers(_getScore);
    };

    function _getScore() {
        let score = 1;
        for (let i = 0; i &lt; presenter.numberOfItems; i++) {
            if (!(presenter.currentPercents[i] == presenter.items[i]["Answer"])) {
                score = 0;
                break;
            }
        }
        return score;
    }

    presenter.getState = function(){
        return executeWithoutShownAnswers(_getState);
    };

    function _getState() {
        const itemsData = new Array(presenter.numberOfItems);
        for (i = 0; i &lt; presenter.numberOfItems; i++) {
            itemsData[i] = presenter.$view.find('#item'+(i+1)).attr('d');
        }
        return JSON.stringify({
            isMoved : presenter.isMoved,
            currentPercents : presenter.currentPercents,
            angles : presenter.angles,
            itemsData : itemsData,
            disabled : presenter.disabled,
            isVisible : presenter.isVisible
        });
    }

    function executeWithoutShownAnswers(funcToCall) {
        const wasShowAnswersActive = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        presenter.handleDisplayedAnswers();

        const result = funcToCall();

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        wasGradualShowAnswersActive &amp;&amp; presenter.gradualShowAnswers({"moduleID": presenter.addonID});

        return result;
    }

    presenter.setState = function(state){
        var angle, angle2, angleTmp, x1, y1;
        if (!presenter.error) {
            presenter.isMoved = JSON.parse(state).isMoved;
            presenter.currentPercents = JSON.parse(state).currentPercents;
            presenter.angles = JSON.parse(state).angles;
            const itemsData = JSON.parse(state).itemsData;
            presenter.isVisible = JSON.parse(state).isVisible;
            presenter.updateVisibility();
            presenter.disabled = JSON.parse(state).disabled;
            presenter.disabled ? presenter.disable() : presenter.enable();
            for(i = 0; i &lt; presenter.numberOfItems; i++) {
                ii = (i===0) ? (presenter.numberOfItems-1) : i-1;
                presenter.$view.find('#'+(i+1)).attr("transform", "rotate(" + presenter.angles[i] + ", "+presenter.center+", "+presenter.center+")");
                presenter.$view.find('#item'+(i+1)).attr("d", presenter.generateItemData(presenter.currentPercents[i], presenter.angles[i]));
                angle = (Math.round((presenter.angles[i] - presenter.angles[ii])*100)/100+360)%360;
                angleTmp = presenter.angles[ii] + 0.5*angle + 180;
                if (presenter.currentPercents[i] === 100) {
                    if (presenter.items[i]['Color'] != '') {
                        presenter.$view.find('.graph').attr("style", "fill:"+presenter.items[i]['Color']+"; fill-opacity:1;");
                    } else {
                        presenter.$view.find('.graph').attr("class","graph item item"+(i+1));
                        presenter.$view.find('.graph').attr("style", "fill-opacity:1");
                    }
                    angleTmp += 180;
                }
                if (presenter.values) presenter.changePercent((i+1), angleTmp);
            }
        }
    };

    presenter.showAnswers = function () {
        if (!presenter.activity) {
            return;
        }

        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.handleDisplayedAnswers();
        presenter.isShowAnswersActive = true;
        _showAnswers();
    };

    presenter.gradualShowAnswers = function (eventData) {
        if (!presenter.activity || eventData.moduleID !== presenter.addonID) {
            return;
        }

        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.handleDisplayedAnswers();
        presenter.isGradualShowAnswersActive = true;
        _showAnswers();
    };

    function _showAnswers() {
        presenter.$view.find(".chart").css("visibility", "hidden");
        presenter.drawGraph("showAnswers");
    }

    presenter.hideAnswers = function () {
        if (!presenter.activity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.isShowAnswersActive = false;
        _hideAnswers();
    };

    presenter.gradualHideAnswers = function () {
        if (!presenter.activity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.isGradualShowAnswersActive = false;
        _hideAnswers();
    };

    function _hideAnswers() {
        presenter.$view.find(".chart-show-answers").remove();
        presenter.$view.find(".chart").css("visibility", "visible");
    }

    presenter.handleDisplayedAnswers = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
    };

    presenter.isAnswersDisplayed = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    };

    presenter.getActivitiesCount = function () {
        return presenter.activity ? 1 : 0;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Plot" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Expressions" nameLabel="Plot_property_expressions" type="list">
			<property isLocalized="false" name="id" nameLabel="Plot_property_id" type="string"/>
			<property isLocalized="false" name="expression" nameLabel="Plot_property_expression" type="string"/>
			<property isLocalized="false" name="xMin" nameLabel="Plot_property_x_min" type="int"/>
			<property isLocalized="false" name="xMax" nameLabel="Plot_property_x_max" type="int"/>
			<property isLocalized="false" name="yMin" nameLabel="Plot_property_y_min" type="int"/>
			<property isLocalized="false" name="yMax" nameLabel="Plot_property_y_max" type="int"/>
			<property isLocalized="false" name="selectable" nameLabel="Plot_property_selectable" type="boolean"/>
			<property isLocalized="false" name="correct" nameLabel="Plot_property_correct" type="boolean"/>
			<property isLocalized="false" name="y to x" nameLabel="Plot_property_y_to_x" type="boolean"/>
			<property isLocalized="false" name="hidden" nameLabel="Plot_property_hidden" type="boolean"/>
			<property isLocalized="false" name="color" nameLabel="Plot_property_color" type="string"/>
			<property isLocalized="false" name="mark at length" nameLabel="Plot_property_mark_at_length" type="string"/>
		</property>
		<property name="Variables" nameLabel="Plot_property_variables" type="list">
			<property isLocalized="false" name="plot id" nameLabel="Plot_property_plot_id" type="string"/>
			<property isLocalized="false" name="variable" nameLabel="Plot_property_variable" type="string"/>
			<property isLocalized="false" name="value" nameLabel="Plot_property_value" type="string"/>
			<property isLocalized="false" name="expected" nameLabel="Plot_property_expected" type="string"/>
		</property>
		<property name="Points" nameLabel="Plot_property_points" type="list">
			<property isLocalized="false" name="x value" nameLabel="Plot_property_x_value" type="string"/>
			<property isLocalized="false" name="y value" nameLabel="Plot_property_y_value" type="string"/>
			<property isLocalized="false" name="selected" nameLabel="Plot_property_selected" type="boolean"/>
			<property isLocalized="false" name="correct" nameLabel="Plot_property_correct" type="boolean"/>
			<property isLocalized="false" name="not scored" nameLabel="Plot_property_not_scored" type="boolean"/>
		</property>
		<property isLocalized="false" name="xMin" nameLabel="Plot_property_x_min" type="int"/>
		<property isLocalized="false" name="xMax" nameLabel="Plot_property_x_max" type="int"/>
		<property isLocalized="false" name="yMin" nameLabel="Plot_property_y_min" type="int"/>
		<property isLocalized="false" name="yMax" nameLabel="Plot_property_y_max" type="int"/>
		<property isLocalized="false" name="Grid" nameLabel="Plot_property_grid" type="boolean"/>
		<property isLocalized="false" name="GridStepX" nameLabel="Plot_property_grid_step_x" type="int"/>
		<property isLocalized="false" name="GridStepY" nameLabel="Plot_property_grid_step_y" type="int"/>
		<property isLocalized="false" name="Arrowhead size" nameLabel="Plot_property_arrow_head_size" type="int"/>
		<property isLocalized="false" name="Axis values" nameLabel="Plot_property_axis_values" type="boolean"/>
		<property isLocalized="false" name="Axis x values" nameLabel="Plot_property_axis_x_values" type="string"/>
		<property isLocalized="false" name="Axis y values" nameLabel="Plot_property_axis_y_values" type="string"/>
		<property isLocalized="true" name="X axis description" nameLabel="Plot_property_x_axis_description" type="string"/>
		<property isLocalized="true" name="Y axis description" nameLabel="Plot_property_y_axis_description" type="string"/>
		<property isLocalized="false" name="hide X axis" nameLabel="Plot_property_hide_x_axis" type="boolean"/>
		<property isLocalized="false" name="hide Y axis" nameLabel="Plot_property_hide_y_axis" type="boolean"/>
		<property isLocalized="false" name="Point active area size" nameLabel="Plot_property_points_active_area_size" type="int"/>
		<property isLocalized="false" name="Point radius" nameLabel="Plot_property_points_radius" type="int"/>
		<property isLocalized="false" name="Point outline radius" nameLabel="Plot_property_points_outline_radius" type="int"/>
		<property isLocalized="false" name="Max selected points" nameLabel="Plot_property_max_selected_points" type="int"/>
		<property isLocalized="false" name="Not activity" nameLabel="Plot_property_not_activity" type="boolean"/>
		<property isLocalized="false" name="Free points" nameLabel="Plot_property_free_points" type="boolean"/>
		<property isLocalized="false" name="Asymptote DY" nameLabel="Plot_property_asymptote_dy" type="int"/>
		<property isLocalized="false" name="Broadcast" nameLabel="Plot_property_broadcast" type="string"/>
		<property isLocalized="false" name="Decimal separator" nameLabel="Plot_property_decimal_separator" type="string"/>
		<property isLocalized="false" name="X axis values position" nameLabel="Plot_property_x_axis_values_position" type="int"/>
		<property isLocalized="false" name="Y axis values position" nameLabel="Plot_property_y_axis_values_position" type="int"/>
		<property isLocalized="false" name="Check marks" nameLabel="Plot_property_check_marks" type="{No, One mark, Individual marks}"/>
		<property isLocalized="false" name="Correct marks HTML" nameLabel="Plot_property_correct_marks_html" type="string"/>
		<property isLocalized="false" name="Error marks HTML" nameLabel="Plot_property_error_marks_html" type="string"/>
	</model>
<css>.canvas {
	background-color:#ffffff;
}
/* axis */
.axisText {
	font-family:Verdana, sans-serif; 
	font-size:8pt; 
	fill:#000000;
	stroke-width:0;
}
.axisXText {
	text-anchor: end;
}		
.axisYText {
}	
.axis {
	stroke:#000000;
	stroke-width: 1;
}
.axisArrows {
	fill: #000000;
}
.axisThicksTextX {
	text-anchor: middle;
	dominant-baseline: central
}
.axisThicksTextY {
	text-anchor: left;
	dominant-baseline: central
}
/* grid */
.grid {
	stroke:#ddd;
	stroke-width: 1;
    stroke-dasharray: 2 2;
}
/* plot */
.draw_active_area {
	stroke-opacity:0.0;
	stroke-linecap:round;
	stroke-linejoin:round;
	stroke-width:10;
	fill:none;
	stroke:#ffffff;
}
.draw_outline_base {
	stroke-linecap:round;
	stroke-linejoin:round;
	stroke-opacity:0;
	fill:none;
}
.draw_outline {
	stroke-opacity:0.5;
	stroke-width:5;
	fill:none;
	stroke:#008800;
}
.draw_outline_mark_error {
	stroke-opacity:0.5;
	stroke-width:5;
	fill:none;			
	stroke:#ff0000;
}
.draw_outline_mark_correct {
	stroke-opacity:0.5;
	stroke-width:5;
	fill:none;
	stroke:#008800;
}
.draw {
	fill: none;
	stroke:#0000ff;
	stroke-width: 2;			
}
.draw_selected {
	fill: none;
	stroke-width:3;
	stroke:#008800;
}
.draw_over{
	stroke:#888888;
}	
.draw_mark_error {
	stroke: #ff0000;
    stroke-width:2;
}
.draw_mark_correct {
	stroke:#008800;
}
/* point */
.point_active_area {
	stroke-width:0;
	fill:#ff0000;
	fill-opacity:0;
}
.point_outline_base {
	stroke-opacity:0;
	stroke:#bb0000;
	stroke-width:2;
	fill-opacity:0;
	fill:#dd0000;
}		
.point_outline {
	stroke-opacity:1;
	stroke:#006600;
	stroke-width:2;
	fill-opacity:1;
	fill:#00bb00;
}	
.point_outline_mark_error {
	stroke-opacity:1;
	stroke:#bb0000;
	stroke-width:2;
    fill-opacity:1;
	fill:#ff0000;
}
.point_outline_mark_correct {
	stroke-opacity:1;
	stroke:#006600;
	stroke-width:2;
	fill-opacity:1;
	fill:#00bb00;
}
.point {
	fill:#dddddd;
	stroke:#bbbbbb;
	stroke-width:0.5;
}
.point_selected {
	fill:#00bb00;
	stroke:#006600;
	stroke-width:2;
}
.point_over {
	fill:#aaaaaa;
	stroke:#666666;
	stroke-width:2;
}
.point_error {
	fill:#ff0000;
	stroke:#bb0000;
	stroke-width:2;
}
.point_correct {
	fill:#00bb00;
	stroke:#00bb00;
	stroke-width:2;
}

.plot_show_answers {
    stroke-opacity:1;
    stroke:#006600;
    stroke-width:2;
    fill-opacity:1;
    fill:#00bb00;
}

.canvas .mark.addon_mark.mark_error{
	color: #ff0000;
}

.canvas .mark.addon_mark.mark_correct{
	color: #00bb00;
}

.canvas .mark.addon_mark {
	position: absolute;
	left: 90%;
	top: 10%;
}

.canvas .mark.point_mark {
	transform: scale(1, -1) translate(8px, 0px);
	dominant-baseline: text-after-edge;
}

.canvas .mark.expression_mark {
	transform: scale(1, -1);
	dominant-baseline: hanging;
}

.canvas .mark.point_mark.mark_correct,
.canvas .mark.expression_mark.mark_correct {
	fill:#00bb00;
	stroke:#00bb00;
	stroke-width:2;
}

.canvas .mark.point_mark.mark_error,
.canvas .mark.expression_mark.mark_error {
	fill:#ff0000;
	stroke:#bb0000;
	stroke-width:2;
}

.canvas .mark {
	font-size: 16px;
}
</css><view>&lt;div class="canvas" style="width:100%; height:100%; overflow:hidden"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="canvas" style="width:100%; height:100%; overflow:hidden"&gt;
&lt;/div&gt;
</preview><presenter>function AddonPlot_create(){
    function Plot() {
        this.VERSION = '1.1.17';
        this.STATE_CORRECT = 1;
        this.STATE_INCORRECT = 0;
        this.STATE_NOT_ACTIVITY = '';
        this.STATE_NULL = 0;
        this.STATE_SELECT_POINT = 1;
        this.STATE_DESELECT_POINT = 0;
        this.INFINITY_NEGATIVE_VALUE = -2147483647;
        this.INFINITY_POSITIVE_VALUE = 2147483647;
        this.ASYMPTOTE_MINIMUM_TRIAL = 3;
        this.TYPE_X_TO_Y = 1;
        this.TYPE_Y_TO_X = 2;
        this.asymptoteMinimumDY = 5;
        this.interactive = true;
        this.xMin = -10;
        this.xMax = 10;
        this.yMin = -10;
        this.yMax = 10;
        this.zoomStep = 0.1;
        this.expressions = [];
        this.points = [];
        this.svg = null;
        this.stepX = 0;
        this.stepY = 0;
        this.offsetX = 0;
        this.offsetY = 0;
        this.width = 0;
        this.height = 0;
        this.gridStepX = 1;
        this.gridStepY = 1;
        this.grid = true;
        this.lastOffsetX = 0;
        this.lastOffsetY = 0;
        this.arrowheadSize = 6;
        this.axisValues = true;
        this.svgDoc = null;
        this.uiEnabled = true;
        this.initXMin = this.xMin;
        this.initXMax = this.xMax;
        this.initYMin = this.yMin;
        this.initYMax = this.yMax;
        this.pointActiveArea = 15;
        this.pointRadius = 3;
        this.pointOutlineRadius = 7;
        this.selectedPoints = [];
        this.xAxisDescription = 'x';
        this.yAxisDescription = 'y';
        this.xAxisVisible = true;
        this.yAxisVisible = true;
        this.xAxisCyclicValues = [];
        this.xAxisCustomValues = [];
        this.yAxisCyclicValues = [];
        this.yAxisCustomValues = [];
        this.maxSelectedPoints = 0;
        this.stateChanged = function(){};
        this.convertValueToDisplay = function(val){ return val; };
        this.isActivity = true;
        this.freePoints = false;
        this.precision = {x: 100, y:100};
        this.attempted = false;
        this.xAxisValuesPosition = 0;
        this.yAxisValuesPosition = 0;

        this.CHECKMARKS = {
            NO: "No",
            ONE_MARK: "One mark",
            INDIVIDUAL_MARKS: "Individual marks"
        };

        this.setXAxisValuesPosition = function (value) {
            this.svgDoc.find('.axisThicksTextX').attr('y', value);
        };

        this.setYAxisValuesPosition = function (value) {
            var element = this.svgDoc.find('.axisThicksTextY');
            element.attr('x', value);
            if(value &lt; 0) {
                element.attr("text-anchor", "end");
            }
        };

        this.setScale = function () {
            this.svgDoc.find('.scale').attr('transform', 'scale(1, -1)');
        };
        this.setStep = function () {
            this.stepX = this.width/(Math.max(this.xMin, this.xMax) - Math.min(this.xMin, this.xMax));
            this.stepY = this.height/(Math.max(this.yMin, this.yMax) - Math.min(this.yMin, this.yMax));
        };
        this.translate = function() {
            this.offsetX = this.xMin*this.stepX*(-1);
            this.offsetY = this.yMax*this.stepY*(-1);
            this.svgDoc.find('.translate').attr('transform', 'translate('+this.offsetX+', '+this.offsetY+')');
        };
        this.draw = function() {
            this.clearCanvas();
            if(this.grid) {
                this.drawGrid();
            }
            this.drawAxis();
            this.drawPlots();
            if(this.points.length &gt; 0 || this.freePoints) {
                this.drawPoints();
            }
            this.enableUI(this.uiEnabled);
            if(this.xAxisValuesPosition != '' &amp;&amp; !isNaN(this.xAxisValuesPosition)){
                this.setXAxisValuesPosition(this.xAxisValuesPosition);
            }

            if(this.yAxisValuesPosition != '' &amp;&amp; !isNaN(this.yAxisValuesPosition)){
                this.setYAxisValuesPosition(this.yAxisValuesPosition);
            }
        };
        this.clearCanvas = function() {
            this.svgDoc.find('.drawings').children().remove();
            this.svgDoc.find('.grid').children().remove();
            this.svgDoc.find('.axis').children().remove();
            this.svgDoc.find('.points').children().remove();
            this.svgDoc.find('.pointsAreas').children().remove();
            this.svgDoc.find('.drawingsAreas').children().remove();
        }
        this.drawXAxis = function() {
            var path = this.svg.createPath();
            this.svg.line(this.svgDoc.find('.axis'), -this.offsetX, 0, this.width-this.offsetX, 0);
            path.move(this.width - this.offsetX, 0).
            line(this.width - this.offsetX - this.arrowheadSize, this.arrowheadSize/2).
            line(this.width - this.offsetX - this.arrowheadSize, -this.arrowheadSize/2).
            line(this.width - this.offsetX, 0);
            this.svg.text(this.svgDoc.find('.axis'), this.width-this.offsetX-3, 13, this.xAxisDescription, {
                'class':'axisText axisXText',
                transform:'scale(1,-1)'
            });
            this.svg.path(this.svgDoc.find('.axis'), path, {
                'class':'axisArrows'
            });
        };
        this.drawYAxis = function() {
            var path = this.svg.createPath();
            this.svg.line(this.svgDoc.find('.axis'), 0, -this.offsetY, 0, -this.height-this.offsetY);
            path.move(0, -this.offsetY).
            line(this.arrowheadSize/2, -this.offsetY - this.arrowheadSize).
            line(-this.arrowheadSize/2, -this.offsetY - this.arrowheadSize).
            line(0, -this.offsetY);
            this.svg.text(this.svgDoc.find('.axis'), 7, this.offsetY+10, this.yAxisDescription, {
                'class':'axisText axisYText',
                transform:'scale(1,-1)'
            });
            this.svg.path(this.svgDoc.find('.axis'), path, {
                'class':'axisArrows'
            });
        };
        this.drawAxis0Point = function() {
            this.svg.text(this.svgDoc.find('.axis'), 3, 13, '0', {
                'class':'axisText',
                transform:'scale(1,-1)'
            });
        };
        this.drawXTick = function(x) {
            if(x &lt; this.width - this.offsetX - this.arrowheadSize) {
                this.svg.line(this.svgDoc.find('.axis'), x, -3, x, 3);
            }
        };
        this.drawYTick = function(y) {
            if(y &lt; -this.offsetY - this.arrowheadSize) {
                this.svg.line(this.svgDoc.find('.axis'), -3, y, 3, y);
            }
        };
        this.drawXTickValue = function(x, coords) {
            if(x &lt; this.width - this.offsetX - 15) {
                if(this.xAxisVisible &amp;&amp; this.yAxisVisible &amp;&amp; coords.x == 0) {
                    return;
                } else {
                    this.svg.text(this.svgDoc.find('.axis'), x, 10, this.convertValueToDisplay(coords.x.toString()), {
                        'class':'axisText axisThicksTextX',
                        transform:'scale(1,-1)'
                    });
                }
            }
        };
        this.drawYTickValue = function(y, coords) {
            if(this.xAxisVisible &amp;&amp; this.yAxisVisible &amp;&amp; coords.y == 0) {
            ;
            } else {
                this.svg.text(this.svgDoc.find('.axis'), 7, y, this.convertValueToDisplay(coords.y.toString()), {
                    'class':'axisText axisThicksTextY',
                    transform:'scale(1,-1)'
                });
            }
        };
        this.checkAndDrawXTickValue = function(x, dx, cx, tx) {
            var currentTick;
            var coords = this.px2coords(cx, 0);
            //check cyclic values
            if(this.xAxisCyclicValues.length &gt; 0) {
                currentTick = Math.round(x/dx);
                $.each(this.xAxisCyclicValues, function(k, v) {
                    if(currentTick%v == 0) {
                        plot.drawXTickValue(tx, coords);
                        return;
                    }
                })
            } else if(this.xAxisCustomValues.length == 0) {
                this.drawXTickValue(tx, coords);
            }
        };
        this.drawCustomXTicks = function() {
            $.each(this.xAxisCustomValues, function(idx, val) {
                p = plot.coords2px(val, 0);
                plot.drawXTick(p.x);
                plot.drawXTickValue(p.x, {x:val, y:0});
            });
        };
        this.drawXTicks = function() {
            var x, sp;
            var dx = this.gridStepX*this.stepX;
            var modX = this.offsetX%(dx);
            //get starting point of viewport
            sp = this.offsetX &lt; 0 ? -this.offsetX + modX: 0
            //above 0
            for(x = sp; x &lt;= this.width - this.offsetX; x += dx) {
                this.drawXTick(x);
                this.checkAndDrawXTickValue(x, dx, x+this.offsetX, x);
            }
            //below 0
            for(x = sp; x &gt;= -this.offsetX - modX; x-= dx) {
                this.drawXTick(x);
                this.checkAndDrawXTickValue(x, dx, x+this.offsetX, x);
            }
            this.drawCustomXTicks();
        };
        this.checkAndDrawYTickValue = function(y, dy, cy, ty) {
            coords = this.px2coords(0, cy);
            //check cyclic values
            if(this.yAxisCyclicValues.length &gt; 0) {
                currentTick = Math.round(y/dy);
                $.each(this.yAxisCyclicValues, function(k, v) {
                    if(currentTick%v == 0) {
                        plot.drawYTickValue(ty, coords);
                        return;
                    }
                })
            } else if(this.yAxisCustomValues.length == 0) {
                this.drawYTickValue(ty, coords);
            }
        };
        this.drawCustomYTicks = function() {
            var p;
            $.each(this.yAxisCustomValues, function(idx, val){
                p = plot.coords2px(0, val);
                plot.drawYTick(p.y);
                plot.drawYTickValue(-p.y, {
                    x:0,
                    y:val
                });
            });
        };
        this.drawYTicks = function() {
            var y;
            var dy = this.gridStepY*this.stepY;
            var modY = this.offsetY%(dy);
            sp = Math.abs(this.offsetY) &lt; this.height ? 0 : Math.abs(this.offsetY) - this.height - (Math.abs(this.offsetY) - this.height)%dy;
            //above 0
            for(y = sp; y &lt;= sp+this.height+dy; y += dy) {
                this.drawYTick(y);
                this.checkAndDrawYTickValue(y, dy, -y-this.offsetY, -y);
            }
            //below 0
            sp = this.offsetY &gt; 0 ? this.offsetY - modY : 0;
            for(y = sp; y &lt; this.offsetY + this.height; y += dy) {
                this.drawYTick(-y);
                this.checkAndDrawYTickValue(y, dy, y-this.offsetY, y);
            }
            this.drawCustomYTicks();
        };
        this.drawAxis = function() {
            if(this.yAxisVisible) {
                this.drawYAxis();
            }
            if(this.xAxisVisible) {
                this.drawXAxis();
            }
            if(this.xAxisVisible &amp;&amp; this.yAxisVisible) {
                this.drawAxis0Point();
            }
            if(this.axisValues) {
                if(this.xAxisVisible) {
                    this.drawXTicks();
                }
                if(this.yAxisVisible) {
                    this.drawYTicks();
                }
            }
        };
        this.drawGrid = function() {
            var dx = this.gridStepX*this.stepX;
            var modX = this.offsetX%(dx);
            for(var x = -this.offsetX+modX; x &lt;= this.width-this.offsetX; x += dx) {
                //pionowe
                this.svg.line(this.svgDoc.find('.grid'), x, -this.offsetY, x, -this.height-this.offsetY);
            }

            var dy = this.gridStepY*this.stepY;
            var modY = this.offsetY%(dy);
            for(var y = -this.offsetY+modY; y &gt;= -this.height-this.offsetY; y -= dy) {
                //poziome
                this.svg.line(this.svgDoc.find('.grid'), -this.offsetX, y, this.width-this.offsetX, y);
            }
        };
        this.drawPoints = function() {
            var dx = this.gridStepX*this.stepX;
            var modX = this.offsetX%(dx);
            var dy = this.gridStepY*this.stepY;
            var modY = this.offsetY%(dy);
            var point;
            var html = $('&lt;span class="point"&gt;&lt;/span&gt;');
            for(var x = -this.offsetX+modX; x &lt;= this.width-this.offsetX; x += dx) {
                var idx = 0;
                for(var y = -this.offsetY+modY; y &gt;= -this.height-this.offsetY; y -= dy) {
                    point = this.px2coords(x+this.offsetX,dy*idx-modY);
                    this.svg.circle(this.svgDoc.find('.points'), x, y, this.pointOutlineRadius, {
                        'class':'point_outline_base',
                        vx:point.x,
                        vy:point.y
                        });
                    this.svg.circle(this.svgDoc.find('.points'), x, y, this.pointRadius, {
                        'class':'point',
                        vx:point.x,
                        vy:point.y
                        });
                    if(this.interactive) {
                        this.svg.rect(this.svgDoc.find('.pointsAreas'), x-this.pointActiveArea/2, y-this.pointActiveArea/2, this.pointActiveArea, this.pointActiveArea, {
                            'class':'point_active_area',
                            vx:point.x,
                            vy:point.y
                            });
                    }
                    idx++;
                }
            }
            $.each(plot.selectedPoints, function(k, v) {
                var refObj = plot.svgDoc.find('.point[vx="'+v.x+'"][vy="'+v.y+'"]');
                var refObjOutline = plot.svgDoc.find('.point_outline_base[vx="'+v.x+'"][vy="'+v.y+'"]');
                refObj.addClass('point_selected');
                refObjOutline.addClass('point_outline');
                if(!v.clickable) {
                    refObj.addClass('nonclickable');
                }
            });

            if(this.interactive) {
                obj = this.svgDoc.find('.point_active_area');
                obj.mouseover(function() {
                    var vx = $(this).attr('vx');
                    var vy = $(this).attr('vy');
                    var point;
                    var point = plot.svgDoc.find('.point[vx="'+vx+'"][vy="'+vy+'"]');
                    if(plot.maxSelectedPoints == 0 || plot.selectedPoints.length &lt; plot.maxSelectedPoints || point.hasClass('point_selected')) {
                        point = plot.svgDoc.find('.point[vx="'+vx+'"][vy="'+vy+'"]');
                        if(!point.hasClass('nonclickable')) {
                            point.addClass('point_over');
                        }
                    }
                });
                obj.mouseout(function() {
                    var vx = $(this).attr('vx');
                    var vy = $(this).attr('vy');
                    plot.svgDoc.find('.point[vx="'+vx+'"][vy="'+vy+'"]').removeClass('point_over');
                });
                obj.click(function(e) {
                    e.stopPropagation();

                    var vx = $(this).attr('vx');
                    var vy = $(this).attr('vy');
                    var refObj = plot.svgDoc.find('.point[vx="'+vx+'"][vy="'+vy+'"]');
                    if(!refObj.hasClass('nonclickable')) {
                        if(refObj.hasClass('point_selected')) {
                            plot._deselectPoint(vx, vy, false);
                        } else {
                            plot._selectPoint(vx, vy, false);
                        }
                    }
                });
            }
        };
        this._touchPoint = function(x, y) {
            this.attempted = true;
            var pvx = parseFloat(x);
            var pvy = parseFloat(y);
            //mark as touched in model if exists
            $.each(plot.points, function(k,v) {
                if(v.x == pvx &amp;&amp; v.y == pvy) {
                    v.touched = true;
                }
            });
        }
        this._deselectPoint = function(x,y, isShowAnswersMode) {
            if (!isShowAnswersMode) {
                 this._touchPoint(x, y);
            }
            var pvx = parseFloat(x);
            var pvy = parseFloat(y);
            var refObj = plot.svgDoc.find('.point[vx="'+x+'"][vy="'+y+'"]');
            var refObjOutline = plot.svgDoc.find('.point_outline_base[vx="'+x+'"][vy="'+y+'"]');
            refObj.removeClass('point_selected');
            refObjOutline.removeClass('point_outline');
            $.each(this.selectedPoints, function(k, v) {
                if(v.x == pvx &amp;&amp; v.y == pvy) {
                    plot.selectedPoints.splice(k, 1);
                    if (!isShowAnswersMode) {
                        plot.stateChanged({
                            item:'point_'+pvx+'_'+pvy,
                            value:0,
                            score:plot.getPointEventScore(pvx, pvy, plot.STATE_DESELECT_POINT)
                        });
                    }
                    return false;
                }
            });
        }
        this._selectPoint = function(x,y, isShowAnswersMode) {
            if (!isShowAnswersMode) {
                this._touchPoint(x, y);
            }
            var pvx = parseFloat(x);
            var pvy = parseFloat(y);
            var refObj = plot.svgDoc.find('.point[vx="'+x+'"][vy="'+y+'"]');
            var refObjOutline = plot.svgDoc.find('.point_outline_base[vx="'+x+'"][vy="'+y+'"]');
            //check if we can add another point
            if(plot.maxSelectedPoints == 0 || plot.selectedPoints.length &lt; plot.maxSelectedPoints) {
                refObj.addClass('point_selected');
                refObjOutline.addClass('point_outline');
                var hasPoint = false;
                $.each(plot.selectedPoints, function(k, v) {
                    if(v.x == pvx &amp;&amp; v.y == pvy) {
                        hasPoint = true;
                        return false;
                    }
                });
                if(!hasPoint) {
                    plot.selectedPoints.push({
                        x:pvx,
                        y:pvy,
                        clickable: true
                    });
                    if (!isShowAnswersMode) {
                        plot.stateChanged({
                            item:'point_'+pvx+'_'+pvy,
                            value:1,
                            score:plot.getPointEventScore(pvx, pvy, plot.STATE_SELECT_POINT)
                        });
                    }
                }
            }
        }
        this.removePlot = function(p) {
            this.svgDoc.find('.drawingsAreas .draw_active_area[refuid="'+p+'"]').remove();
            this.svgDoc.find('.drawings .draw_outline_base[ouid="'+p+'"]').remove();
            this.svgDoc.find('.drawings .draw[uid="'+p+'"]').remove();
        }
        this.drawPlot = function(p) {
            var obj, cp;
            var path = this.svg.createPath();
            //function type x to y
            if(this.expressions[p].type == this.TYPE_X_TO_Y) {
                path = this._drawXToY(p);
            //function type y to x
            } else if(this.expressions[p].type == this.TYPE_Y_TO_X) {
                path = this._drawYToX(p);
            }
            this.svg.path(this.svgDoc.find('.drawings'), path, {
                ouid:p,
                'class': 'draw_outline_base'
            });
            this.svg.path(this.svgDoc.find('.drawings'), path, {
                uid:p,
                isselected: this.expressions[p].selected ? 1 : 0,
                'class': 'is_plot draw draw_'+(parseInt(p)+1)
            });
            //restore style
            cp = plot.svgDoc.find('.drawings [uid="'+p+'"]');
            var props = this.expressions[p].cssProperties;
            props['stroke'] = this.expressions[p].cssColor;
            cp.data({
                cssStyle: this._composeStyle(props)
            });
            cp.attr('style', cp.data().cssStyle);
            if(this.interactive) {
                this.svg.path(this.svgDoc.find('.drawingsAreas'), path, {
                    refuid:p,
                    'class':'draw_active_area'
                });
            }
            if(this.expressions[p].selected) {
                plot.svgDoc.find('.is_plot[uid="'+p+'"]').addClass('draw_selected draw_'+(parseInt(p)+1)+'_selected').removeAttr('style');
                plot.svgDoc.find('.draw_outline_base[ouid="'+p+'"]').addClass('draw_outline draw_'+(parseInt(p)+1)+'_outline');
            }
            if(this.expressions[p].selectable &amp;&amp; this.interactive) {
                obj = this.svgDoc.find('.draw_active_area[refuid="'+p+'"]');
                obj.mouseover(function() {
                    var id = parseInt($(this).attr('refuid'));
                    p = plot.svgDoc.find('.is_plot[uid="'+id+'"]');
                    p.addClass('draw_over draw_'+(id+1)+'_over').removeAttr('style');
                });
                obj.mouseout(function() {
                    var id = parseInt($(this).attr('refuid'));
                    p = plot.svgDoc.find('.is_plot[uid="'+id+'"]');
                    p.removeClass('draw_over draw_'+(id+1)+'_over');
                    if(p.attr('isselected') != 1) {
                        p.attr('style', p.data().cssStyle);
                    }
                });
                obj.click(function(e) {
                    e.stopPropagation();

                    var id = parseInt($(this).attr('refuid'));
                    var refObj = plot.svgDoc.find('.is_plot[uid="'+id+'"]');
                    var refObjOutline = plot.svgDoc.find('.draw_outline_base[ouid="'+id+'"]');
                    var selected = parseInt(refObj.attr('isselected'));
                    plot.expressions[id].touched = true;
                    plot.attempted = true;
                    if(selected) {
                        refObj.attr('isselected', 0);
                        refObj.attr('style', refObj.data().cssStyle);
                        refObj.removeClass('draw_selected draw_'+(id+1)+'_selected');
                        refObjOutline.removeClass('draw_outline draw_'+(id+1)+'_outline');
                        plot.expressions[id].selected = false;
                        plot.stateChanged({
                            item:'plot_'+plot.expressions[id].id,
                            value:0,
                            score: plot.getPlotEventScore(plot.expressions[id].id)
                        });
                    } else {
                        refObj.attr('isselected', 1);
                        refObj.addClass('draw_selected draw_'+(id+1)+'_selected');
                        refObj.removeAttr('style');
                        refObjOutline.addClass('draw_outline draw_'+(id+1)+'_outline');
                        plot.expressions[id].selected = true;
                        plot.stateChanged({
                            item:'plot_'+plot.expressions[id].id,
                            value:1,
                            score: plot.getPlotEventScore(plot.expressions[id].id)
                        });
                    }
                });
            }
        }
        this.areIndividualMarks = function () {
            return this.checkMarks === this.CHECKMARKS.INDIVIDUAL_MARKS;
        }
        this.isOneMark = function () {
            return this.checkMarks === this.CHECKMARKS.ONE_MARK;
        }
        this.createIndividualMarksSVGDOMGroup = function () {
            const $groupsParent = this.svgDoc.find(".overall");
            this.svg.group($groupsParent, {
                class_: "marks"
            });
        }
        this.drawOneCorrectMark = function () {
            this.drawOneMark(true);
        }
        this.drawOneErrorMark = function () {
            this.drawOneMark(false);
        }
        this.drawOneMark = function (isCorrect) {
            const additionalClassName = isCorrect ? "mark_correct" :  "mark_error";
            const innerHTML = isCorrect ? this.correctMarksHTML : this.errorMarksHTML;

            const addonMark = document.createElement("div");
            addonMark.classList.add("mark");
            addonMark.classList.add("addon_mark");
            addonMark.classList.add(additionalClassName);
            $(addonMark).html(window.xssUtils.sanitize(innerHTML));
            this.svgDoc.parent().append(addonMark);
        }
        this.drawCorrectMarkForPoint = function (x, y) {
            this.drawMarkForPoint(x, y, true);
        }
        this.drawErrorMarkForPoint = function (x, y) {
            this.drawMarkForPoint(x, y, false);
        }
        this.drawMarkForPoint = function (x, y, isCorrect) {
            this.svgDoc.find('.marks .mark.point_mark[vx="' + x + '"][vy="' + y + '"]').remove();

            const additionalClassName = isCorrect ? "mark_correct" :  "mark_error";
            const innerHTML = isCorrect ? this.correctMarksHTML : this.errorMarksHTML;
            const $source = this.svgDoc.find('.point[vx="' + x + '"][vy="' + y + '"]');

            this.svg.text(this.svgDoc.find(".marks"), "",  {
                "x": $source.attr("cx"),
                "y": -$source.attr("cy"),
                "vx": x,
                "vy": y,
                "class": "mark point_mark " + additionalClassName,
            });

            const $text = this.svgDoc.find('.marks .mark.point_mark[vx="' + x + '"][vy="' + y + '"]');
            $text.html(window.xssUtils.sanitize(innerHTML));
        }
        this.drawCorrectMarkForExpression = function(idx) {
            this.drawMarkForExpression(idx, true);
        }
        this.drawErrorMarkForExpression = function(idx) {
            this.drawMarkForExpression(idx, false);
        }
        this.drawMarkForExpression = function (idx, isCorrect) {
            this.svgDoc.find('.marks .mark.expression_mark[refuid="' + idx + '"]').remove();

            const additionalClassName = isCorrect ? "mark_correct" :  "mark_error";
            const innerHTML = isCorrect ? this.correctMarksHTML : this.errorMarksHTML;

            const path = this.svgDoc.find('.is_plot[uid="'+idx+'"]')[0];
            const pathDistance = path.getTotalLength() * this.expressions[idx].markAtLength;
            const midpoint = path.getPointAtLength(pathDistance);

            this.svg.text(this.svgDoc.find(".marks"), "",  {
                "refuid": idx,
                "class": "mark expression_mark " + additionalClassName,
            });

            const $text = this.svgDoc.find('.marks .mark.expression_mark[refuid="' + idx + '"]');
            $text.attr("x", midpoint.x);
            $text.attr("y", -midpoint.y);
            $text.html(window.xssUtils.sanitize(innerHTML));
        }
        this.removeMarks = function() {
            this.svgDoc.find(".marks").remove();
            $(presenter.view).find(".mark").remove();
        }
        this.drawPlots = function() {
            $.each(this.expressions, function(idx, val) {
                if(val.visible) {
                    plot.drawPlot(idx);
                }
            });
        };
        this._drawYToX = function(p) {
            var path = this.svg.createPath();
            var rx, ry, i;
            var idx = 0;
            var values = [];
            var sy = -this.offsetY;
            var ey = -this.height-this.offsetY;
            var domain = this._getPlotDomain(p);
            var xMin = domain.xMin;
            var xMax = domain.xMax;
            var yMin = domain.yMin;
            var yMax = domain.yMax;

            //create variables for parser
            var variables = this._mapPlotVariables(p);

            for(i = sy; i &gt;= ey; i--) {
                ry = this.yMax-(idx)/this.stepY;
                //add x to parser variables
                variables.y = ry;
                try {
                    rx = Parser.evaluate(this.expressions[p].expression, variables);
                } catch (e) {
                    break;
                }
                if(rx &gt; this.INFINITY_POSITIVE_VALUE) {
                    //console.log(i,ry,'-n');
                    rx = Infinity;
                }
                if(rx &lt; this.INFINITY_NEGATIVE_VALUE) {
                    //console.log(i,ry,'+n');
                    rx = -Infinity;
                }

                values.push({
                   rx: rx.toFixed(5),
                   ry: ry.toFixed(5),
                   i: i
                });

                idx++;
            }

            var d1 = d2 = d3 = dX = 0;
            var asymptote = false;
            var positioning = true; //set position at the beginning of drawing
            var closedAsymptote = false;
            var prevXMin = prevXMax = prevYMin = prevYMax = false;

            for(var j = 0; j&lt;values.length; j+=1) {
                ry = values[j].ry;
                rx = values[j].rx;
                i = values[j].i;

                //yMin is set
                if(yMin !== null) {
                    //range check
                    if(ry &lt; yMin) {
                        prevYMin = ry;
                        positioning = true;
                        continue;
                    }
                    //if we pass from out of range to in range
                    if(prevYMin !== false &amp;&amp; prevYMin &lt; yMin &amp;&amp; ry &gt;= yMin) {
                        prevYMin = false;
                        //console.log('przejscie xmin',i,rx)
                        //calculate equation for xMin
                        variables.y = yMin;
                        try {
                            rx = Parser.evaluate(this.expressions[p].expression, variables);
                        } catch (e) {
                            break;
                        }
                        //force to draw y at draw point x
                        i = this.coords2px(0, yMin).y;
                    }
                }

                //yMax is set
                if(yMax !== null) {
                    //we are in range
                    if(ry &lt;= yMax) {
                        prevYMax = ry;
                    }
                    //we pass xMax
                    if(prevYMax !== false &amp;&amp; prevYMax &lt;= yMax &amp;&amp; ry &gt; yMax) {
                        prevYMax = false;
                        //console.log('przejscie xmax',i,rx)
                        //calculate equation for xMin
                        variables.y = yMax;
                        try {
                            rx = Parser.evaluate(this.expressions[p].expression, variables);
                        } catch (e) {
                            break;
                        }
                        //force draw x value
                        i = this.coords2px(0,yMax).y;
                    } else if(ry &gt; yMax) {
                        positioning = true;
                        prevYMax = false;
                        continue;
                    }
                }

                if(xMin !== null) {
                    //range check
                    if(rx &lt; xMin) {
                        prevXMin = rx;
                    }
                    //if we pass from out of range to in range - open path with yMin
                    if(prevXMin !== false &amp;&amp; prevXMin &lt; xMin &amp;&amp; rx &gt;= xMin) {
                        positioning = true;
                        prevXMin = false;
                        //console.log('wejscie xmin',i,rx,ry);
                        rx = xMin;
                    } else if(j&gt;0 &amp;&amp; values[j-1].rx &gt;= xMin &amp;&amp; rx &lt; xMin)  {
                        //if we pass from in range to out of range (close path)
                        positioning = false;
                        //console.log('zejscie xmin', i, rx, ry);
                        rx = xMin;
                    } else if( rx &lt; xMin){
                        continue;
                    }
                }

                if(xMax !== null) {
                    if(rx &lt;= xMax) {
                        prevXMax = rx;
                    }
                    //we pass yMax
                    if(prevXMax !== false &amp;&amp; prevXMax &lt;= xMax &amp;&amp; rx &gt; xMax) {
                        prevXMax = false;
                        //console.log('zejscie poza xmax',j,i,rx,ry)
                        rx = xMax;
                    } else if(j &gt; 0 &amp;&amp; values[j-1].rx &gt; xMax &amp;&amp; rx &lt;= xMax) {
                        //console.log('wejscie z poza xmax',j,i,rx,ry,values[j-1].ry )
                        positioning = true;
                        rx = xMax;
                    } else if(rx &gt; xMax) {
                        positioning = true;
                        prevXMax = false;
                        continue;
                    }
                }

                if(j &gt; this.ASYMPTOTE_MINIMUM_TRIAL) {
                    dX = rx-values[j-1].rx;
                    d3 = values[j-1].rx - values[j-2].rx;
                    d2 = values[j-2].rx - values[j-3].rx;
                    d1 = values[j-3].rx - values[j-4].rx;
                }

                //detect discontinuous
                if(this.isDiscontinuous(d1, d2, d3, dX)) {
                    rx = Number.NaN;
                }

                //detect asymptote
                asymptote=this.hasAsymptote(d1, d2, d3, dX);

                if(asymptote.asymptote) {
                    //console.log('Asymptote: ',i, j,asymptote.asymptote,asymptote.val, rx,ry,'d:',d1,d2,d3,dX);
                    //draw graph to -+Infinity
                    //draw to -Infinity
                    if(!closedAsymptote) {
                        //draw to -Infinity
                        if(asymptote.val == 1) {
                            //if prevoius value was in range draw closing asymptote line
                            if(j&gt;0 &amp;&amp; values[j-1].rx &gt;= xMin &amp;&amp; rx &lt; xMin) {
                                path.line((this.xMin*this.stepX).toFixed(5), i);
                            }
                            path.move((this.xMax*this.stepX).toFixed(5), i);
                            //console.log(i,'-n closing');
                            closedAsymptote = true;
                        }
                        //draw to +Infinity
                        if(asymptote.val == -1) {
                            //if prevoius value was in range draw closing asymptote line
                            if(j &gt; 0 &amp;&amp; !(values[j-1].rx &gt; xMax &amp;&amp; rx &lt;= xMax)) {
                                path.line((this.xMax*this.stepX).toFixed(5), i);
                            }
                            path.move((this.xMin*this.stepX).toFixed(5), i);
                            //console.log(i,'+n closing');
                            closedAsymptote = true;
                        }
                    }
                } else {
                    closedAsymptote = false;
                    //if point in range
                    if(rx != Infinity &amp;&amp; rx != -Infinity &amp;&amp; !isNaN(rx)) {
                        //set starting point
                        if(positioning) {
                            path.move((rx*this.stepX).toFixed(5), i);
                            positioning = false;
                        }
                        //draw
                        if(!positioning) {
                            path.line((rx*this.stepX).toFixed(5),i);
                        }
                    } else {
                        positioning = true;
                    }
                }
            }

            return path;
        }

        this.isDiscontinuous = function(d1, d2, d3, dY) {
            if(Math.abs(dY) &gt; this.asymptoteMinimumDY &amp;&amp; d1 == 0 &amp;&amp; d2 == 0 &amp;&amp; d3 == 0 &amp;&amp; dY != 0) {
                return true;
            }
        };

        //detect asymptote
        this.hasAsymptote = function(d1, d2, d3, dY) {
            //if dY is greater than minimum step or is equal to +- infinity check deltas
            if(Math.abs(dY) &gt; this.asymptoteMinimumDY || dY == -Infinity || dY == Infinity) {
                if(d1 &gt; 0 &amp;&amp; d2 &gt; 0 &amp;&amp; d3 &gt; 0 &amp;&amp; d1 &lt; d2 &amp;&amp; d2 &lt; d3)	{
                    if(dY &lt; d3) {
                        return {
                            asymptote: true,
                            val: -1
                        };
                    } else {
                        return {
                            asymptote: false,
                            val: 0
                        };
                    }
                }
                if(d1 &lt; 0 &amp;&amp; d2 &lt; 0 &amp;&amp; d3 &lt; 0 &amp;&amp; d1 &gt; d2 &amp;&amp; d2 &gt; d3) {
                    if(dY &gt; d3) {
                        return {
                            asymptote: true,
                            val: 1
                        };
                    } else {
                        return {
                            asymptote: false,
                            val: 0
                        };
                    }
                }
            }

            return {
                asymptote: false,
                val: 0
            };
        }
        this._mapPlotVariables = function(p) {
            var variables = {};
            $.each(this.expressions[p].variables, function(k,v) {
                variables[k] = v.value;
            });

            return variables;
        }
        this._getPlotDomain = function(p) {
            var variables = this._mapPlotVariables(p);
            var xMin = this.expressions[p].xMin !== false ? Parser.evaluate(this.expressions[p].xMin.toString(), variables) : null;
            var xMax = this.expressions[p].xMax !== false ? Parser.evaluate(this.expressions[p].xMax.toString(), variables) : null;
            var yMin = this.expressions[p].yMin !== false ? Parser.evaluate(this.expressions[p].yMin.toString(), variables) : null;
            var yMax = this.expressions[p].yMax !== false ? Parser.evaluate(this.expressions[p].yMax.toString(), variables) : null;
            if(xMin === null || xMin &lt; this.xMin) {
                xMin = this.xMin;
            }
            if(xMax === null || xMax &gt; this.xMax) {
                xMax = this.xMax;
            }
            if(yMin === null || yMin &lt; this.yMin) {
                yMin = this.yMin;
            }
            if(yMax === null || yMax &gt; this.yMax) {
                yMax = this.yMax;
            }

            return {xMin: xMin, xMax:xMax, yMin: yMin, yMax: yMax};
        }
        this._drawXToY = function(p) {
            var path = this.svg.createPath();
            var rx, ry, i;
            var values = [];
            var sx = -this.offsetX;
            var ex = sx+this.width;
            var domain = this._getPlotDomain(p);
            var xMin = domain.xMin;
            var xMax = domain.xMax;
            var yMin = domain.yMin;
            var yMax = domain.yMax;

            //create variables for parser
            var variables = this._mapPlotVariables(p);

            //calculate values for every pixel
            for(i = sx; i &lt;= ex; i++) {
                rx = this.xMin+(i-sx)/this.stepX;
                //add x to parser variables
                variables.x = rx;
                try {
                    ry = Parser.evaluate(this.expressions[p].expression, variables);
                } catch (e) {
                    break;
                }
                if(ry &gt; this.INFINITY_POSITIVE_VALUE) {
                    //console.log(i,ry,'-n');
                    ry = Infinity;
                }
                if(ry &lt; this.INFINITY_NEGATIVE_VALUE) {
                    //console.log(i,ry,'+n');
                    ry = -Infinity;
                }

                values.push({
                   rx: rx.toFixed(5),
                   ry: ry.toFixed(5),
                   i: i
                });
            }

            var d1 = d2 = d3 = dY = 0;
            var asymptote = false;
            var positioning = true; //set position at the beginning of drawing
            var closedAsymptote = false;
            var prevXMin = prevXMax = prevYMin = prevYMax = false;

            for(var j = 0; j&lt;values.length; j+=1) {
                ry = values[j].ry;
                rx = values[j].rx;
                i = values[j].i;

                //xMin is set
                if(xMin !== null) {
                    //range check
                    if(rx &lt; xMin) {
                        prevXMin = rx;
                        positioning = true;
                        continue;
                    }
                    //if we pass from out of range to in range
                    if(prevXMin !== false &amp;&amp; prevXMin &lt; xMin &amp;&amp; rx &gt;= xMin) {
                        prevXMin = false;
                        //console.log('przejscie xmin',i,rx)
                        //calculate equation for xMin
                        variables.x = xMin;
                        try {
                            ry = Parser.evaluate(this.expressions[p].expression, variables);
                        } catch (e) {
                            break;
                        }
                        //force to draw y at draw point x
                        i = this.coords2px(xMin, 0).x;
                    }
                }

                //xMax is set
                if(xMax !== null) {
                    //we are in range
                    if(rx &lt;= xMax) {
                        prevXMax = rx;
                    }
                    //we pass xMax
                    if(prevXMax !== false &amp;&amp; prevXMax &lt;= xMax &amp;&amp; rx &gt; xMax) {
                        prevXMax = false;
                        //console.log('przejscie xmax',i,rx)
                        //calculate equation for xMin
                        variables.x = xMax;
                        try {
                            ry = Parser.evaluate(this.expressions[p].expression, variables);
                        } catch (e) {
                            break;
                        }
                        //force draw x value
                        i = this.coords2px(xMax, 0).x;
                    } else if(rx &gt; xMax) {
                        positioning = true;
                        prevXMax = false;
                        continue;
                    }
                }

                if(yMin !== null) {
                    //range check
                    if(ry &lt; yMin) {
                        prevYMin = ry;
                    }

                    //if we pass from out of range to in range - open path with yMin
                    if(prevYMin !== false &amp;&amp; prevYMin &lt; yMin &amp;&amp; ry &gt;= yMin) {
                        positioning = true;
                        prevYMin = false;
                        //console.log('wejscie ymin',i,rx,ry);
                        ry = yMin;
                    } else if(j&gt;0 &amp;&amp; values[j-1].ry &gt;= yMin &amp;&amp; ry &lt; yMin)  {
                        //if we pass from in range to out of range (close path)
                        positioning = false;
                        //console.log('zejscie ymin', i, rx, ry);
                        ry = yMin;
                    } else if( ry &lt; yMin){
                        continue;
                    }
                }

                if(yMax !== null) {
                    if(ry &lt;= yMax) {
                        prevYMax = ry;
                    }
                    //we pass yMax
                    if(prevYMax !== false &amp;&amp; prevYMax &lt;= yMax &amp;&amp; ry &gt; yMax) {
                        prevYMax = false;
                        //console.log('zejscie poza ymax',j,i,rx,ry)
                        ry = yMax;
                    } else if(j &gt; 0 &amp;&amp; values[j-1].ry &gt; yMax &amp;&amp; ry &lt;= yMax) {
                        //console.log('wejscie z poza ymax',j,i,rx,ry,values[j-1].ry )
                        positioning = true;
                        ry = yMax;
                    } else if(ry &gt; yMax) {
                        positioning = true;
                        prevYMax = false;
                        continue;
                    }
                }

                //calculate deltas
                if(j &gt; this.ASYMPTOTE_MINIMUM_TRIAL) {
                    dY = ry-values[j-1].ry;
                    d3 = values[j-1].ry - values[j-2].ry;
                    d2 = values[j-2].ry - values[j-3].ry;
                    d1 = values[j-3].ry - values[j-4].ry;
                }

                //detect discontinuous
                if(this.isDiscontinuous(d1, d2, d3, dY)) {
                    ry = Number.NaN;
                }
                //detect asymptote
                asymptote=this.hasAsymptote(d1, d2, d3, dY);

                if(asymptote.asymptote) {
                    //console.log('Asymptote: ',i, j,asymptote.asymptote,asymptote.val, rx,ry,'d:',d1,d2,d3,dY);
                    //draw graph to -+Infinity
                    //draw to -Infinity
                    if(!closedAsymptote) {
                        //draw to -Infinity
                        if(asymptote.val == 1) {
                            //if prevoius value was in range draw closing asymptote line
                            if(j&gt;0 &amp;&amp; values[j-1].ry &gt;= yMin &amp;&amp; ry &lt; yMin) {
                                path.line(i, (this.yMin*this.stepY).toFixed(5));
                            }
                            path.move(i, (this.yMax*this.stepY).toFixed(5));
                            //console.log(i,'-n closing');
                            closedAsymptote = true;
                        }
                        //draw to +Infinity
                        if(asymptote.val == -1) {
                            //if prevoius value was in range draw closing asymptote line
                            if(j &gt; 0 &amp;&amp; !(values[j-1].ry &gt; yMax &amp;&amp; ry &lt;= yMax)) {
                                path.line(i, (this.yMax*this.stepY).toFixed(5));
                            }
                            path.move(i, (this.yMin*this.stepY).toFixed(5));
                            //console.log(i,'+n closing');
                            closedAsymptote = true;
                        }
                    }
                }  else {
                    closedAsymptote = false;
                    //if point in range
                    if(ry != Infinity &amp;&amp; ry != -Infinity &amp;&amp; !isNaN(ry)) {
                        //set starting point
                        if(positioning) {
                            path.move(i, (ry*this.stepY).toFixed(5));
                            positioning = false;
                        }
                        //draw
                        if(!positioning) {
                            path.line(i, (ry*this.stepY).toFixed(5));
                        }
                    } else {
                        positioning = true;
                    }
                }
            }

            return path;
        }
        this.reset = function() {
            $.each(this.expressions, function(idx, val) {
                if(val.selectable) {
                    val.selected = false;
                }
                val.touched = false;
            })

            this.selectedPoints = [];
            $.each(this.points, function(idx, val) {
                if(val.initiallySelected) {
                    plot.selectedPoints.push({
                        x: val.x,
                        y: val.y,
                        clickable: val.clickable
                    });
                }
            });
            this.uiEnabled = true;
            this.restoreInitialViewPort();
        }
        this.restoreInitialViewPort = function() {
            this.xMin = this.initXMin;
            this.xMax = this.initXMax;
            this.yMin = this.initYMin;
            this.yMax = this.initYMax;
            this.setStep();
            this.translate();
            this.draw();
        }
        this.coords2px = function(cx, cy) {
            return {
                x:cx*this.stepX,
                y:cy*this.stepY
            }
        };
        this.px2coords = function(px, py) {
            var xRange = Math.max(this.xMin, this.xMax) - Math.min(this.xMin, this.xMax);
            var yRange = Math.max(this.yMin, this.yMax) - Math.min(this.yMin, this.yMax);

            var x = px*xRange/this.width+this.xMin;
            x = Math.round(this.precision.x*x)/this.precision.x;
            var y = (py*yRange/this.height)*(-1)+this.yMax;
            y = Math.round(this.precision.y*y)/this.precision.y;

            return {
                x: x,
                y: y
            };
        };

        this.calculateRange = function() {
            var xRange = Math.max(this.xMin, this.xMax) - Math.min(this.xMin, this.xMax);
            var offsetX = this.lastOffsetX*xRange/this.width
            this.lastOffsetX = 0;
            this.xMin -= offsetX;
            this.xMax -= offsetX;

            var yRange = Math.max(this.yMin, this.yMax) - Math.min(this.yMin, this.yMax);
            var offsetY = this.lastOffsetY*yRange/this.height
            this.lastOffsetY = 0;

            this.yMin -= offsetY;
            this.yMax -= offsetY;

            this.setStep();
        };

        this.calculatePrecision = function() {
            var idx = this.gridStepX.toString().indexOf('.');
            var idy = this.gridStepY.toString().indexOf('.');
            this.precision.x = idx !== -1 ? Math.pow(10, this.gridStepX.toString().length - idx - 1) : 100;
            this.precision.y = idy !== -1 ? Math.pow(10, this.gridStepY.toString().length - idx - 1) : 100;
        };

        /**
         * param int direction - -1 zoomOut, +1 zoomIn
         **/
        this.zoom = function(direction) {
            this.xMin -= this.xMin*this.zoomStep*direction;
            this.xMax -= this.xMax*this.zoomStep*direction;
            this.yMin -= this.yMin*this.zoomStep*direction;
            this.yMax -= this.yMax*this.zoomStep*direction;
            this.setStep();
            this.draw();
        };

        this.zoomIn = function(direction) {
            this.zoom(1);
        }

        this.zoomOut = function(direction) {
            this.zoom(-1);
        }

        this.move = function(dx, dy) {
            this.offsetX += dx;
            this.offsetY += dy;
            this.svgDoc.find('.translate').attr('transform', 'translate('+this.offsetX+', '+this.offsetY+')');
            this.lastOffsetX = dx;
            this.lastOffsetY = dy;
            this.calculateRange();
            this.draw();
        };

        this.moveLeft = function(dx) {
            this.move(parseInt(dx),0);
        }

        this.moveRight = function(dx) {
            this.move(parseInt(dx)*(-1),0);
        }

        this.moveUp = function(dy) {
            this.move(0, parseInt(dy)*(-1));
        }

        this.moveDown = function(dy) {
            this.move(0, parseInt(dy));
        }

        this._composeStyle = function(style) {
            var formattedStyle = '';
            $.each(style, function(o, v) {
                if(v!='') {
                    formattedStyle += o+':'+v+';'
                }
            });
            return formattedStyle;
        }
        this.enableUI = function(state) {
            this.svgDoc.find('.blocker').remove();
            if(state) {
                this.uiEnabled = true;
            } else {
                this.uiEnabled = false;
                this.svg.polygon(this.svgDoc.find('.overall'), [[-this.offsetX,-this.offsetY], [this.width-this.offsetX,-this.offsetY], [this.width-this.offsetX,-this.height-this.offsetY], [-this.offsetX, -this.height-this.offsetY]], {
                    'class':'blocker',
                    fill: '#000000',
                    'fill-opacity':0,
                    strokeWidth: 0
                });
            }
        }
        this.setVariable = function(id, variable, value) {
            if(id != '' &amp;&amp; variable != '') {
                $.each(this.expressions, function(idx, val){
                    if(val.id == id) {
                        if(val.variables[variable] != undefined) {
                            plot.attempted = true;
                            val.variables[variable]['value'] = value != '' ? value : 0;
                            if(val.variables[variable]['isExercise']) {
                                val.variables[variable]['touched'] = true;
                                //call chain of events
                                plot.stateChanged([
                                {
                                    item:'variable_'+val.id+'_'+variable,
                                    value:value,
                                    score: plot.isCorrectVariable(val.id, variable)
                                },

                                {
                                    item:'variables_'+val.id+'_'+variable,
                                    value:'',
                                    score: plot.areCorrectPlotVariables(val.id)
                                }
                                ]);
                            }
                        }
                        plot.removePlot(idx);
                        if(val.visible) {
                            plot.drawPlot(idx);
                        }
                    }

                });
            }
        }
        this.setVisible = function(id, value) {
            if(id != '') {
                $.each(this.expressions, function(idx, val) {
                    if(val.id == id) {
                        val.visible = value;
                        //remove existing plot
                        plot.removePlot(idx);
                        //show plot
                        if(value) {
                            plot.drawPlot(idx);
                        }
                    }
                });
            }
        }
        this.setPointVisibility = function(x, y, visibility) {
            if(x != '' &amp;&amp; y != '') {
                var refObj = plot.svgDoc.find('.point[vx="'+x+'"][vy="'+y+'"]');
                var refObjOutline = plot.svgDoc.find('.point_outline_base[vx="'+x+'"][vy="'+y+'"]');
                if(visibility &amp;&amp; !refObj.hasClass('point_selected')) {
                    this._selectPoint(x, y, false);
                } else if(!visibility &amp;&amp; refObj.hasClass('point_selected')) {
                    this._deselectPoint(x, y, false);
                }
            }
        }
        this.setPlotStyle = function(id, type, prop, value) {
            if(id != '') {
                if(type == 'plot') {
                    $.each(this.expressions, function(idx, val) {
                        if(val.id == id) {
                            if(prop == 'color' || prop == 'stroke') {
                                val.cssColor = value;
                            } else {
                                val.cssProperties[prop] = value;
                            }
                            //remove existing plot
                            plot.removePlot(idx);
                            //show plot
                            if(val.visible) {
                                plot.drawPlot(idx);
                            }
                        }
                    });
                }
            }
        }
        this.plotVariablesResult = function(plotId) {
            //check variables
            var variablesTodo = 0;
            var variablesDone = 0;
            var touched = false;
            $.each(this.expressions[plotId].variables, function(k,v) {
                if(v.isExercise) {
                    variablesTodo++;
                    if(v.expectedValue == v.value) {
                        variablesDone++;
                    }
                    if(v.touched) {
                        touched = true;
                    }
                }
            });

            return {
                todo: variablesTodo,
                done: variablesDone,
                touched: touched
            };
        };

        this.getPlotEventScore = function(pid) {
            if(!this.isActivity) {
                return this.STATE_NOT_ACTIVITY;
            }
            var state = this.STATE_INCORRECT;
            $.each(this.expressions, function(idx, val) {
                if(val.id == pid) {
                    if(val.selected == val.correctAnswer &amp;&amp; val.correctAnswer == true) {
                        state = plot.STATE_CORRECT;
                    } else if((val.correctAnswer == true &amp;&amp; val.selected == false) || (val.correctAnswer == false &amp;&amp; val.selected == false)) {
                        state = plot.STATE_NULL;
                    } else if(val.correctAnswer == false &amp;&amp; val.selected == true) {
                        state = plot.STATE_INCORRECT;
                    }
                    return false;
                }
            });
            return state;
        };
        this.isPointOnPlot = function(pid, x, y) {
            var rv, rc, domain;
            var isCorrect = false;
            $.each(this.expressions, function(idx, val) {
                if (val.id == pid) {
                    var variables = plot._mapPlotVariables(idx);
                    if(val.type == plot.TYPE_X_TO_Y) {
                        rc = y;
                        variables.x = x;
                    } else {
                        rc = x;
                        variables.y = y;
                    }
                    try {
                        rv = Parser.evaluate(plot.expressions[idx].expression, variables);
                    } catch (e) {
                        return false;
                    }

                    isCorrect = rv == rc;

                    //check if point is in range
                    if(isCorrect) {
                        domain = plot._getPlotDomain(idx);
                        if(x &lt; domain.xMin || x &gt; domain.xMax || y &lt; domain.yMin || y &gt; domain.yMax) {
                            isCorrect = false;
                        }
                    }
                    return false;
                }
            });

            return isCorrect;
        };
        this.getPointEventScore = function(vx, vy, state) {
            var currentState = this.STATE_NULL;
            var pointInDefinedPoints = false;
            if(!this.isActivity || (this.points.length == 0 &amp;&amp; this.freePoints)) {
                return this.STATE_NOT_ACTIVITY;
            }

            //check point in model points
            $.each(this.points, function(idx, val) {
                //we have this point
                if(val.x == vx &amp;&amp; val.y == vy) {
                    //check if should be selected or not
                    if((val.correct &amp;&amp; state == plot.STATE_SELECT_POINT) || (!val.correct &amp;&amp; state == plot.STATE_DESELECT_POINT)) {
                        currentState = plot.STATE_CORRECT;
                    } else if(state == plot.STATE_SELECT_POINT) {
                        currentState = plot.STATE_INCORRECT;
                    } else {
                        currentState = plot.STATE_NULL;
                    }
                    pointInDefinedPoints = true;
                    return false;
                }
            });

            if(pointInDefinedPoints) {
                return currentState;
            } else {
                return state == this.STATE_SELECT_POINT ? this.STATE_INCORRECT : this.STATE_NULL;
            }
        };

        this.isCorrectVariable = function(pid, variable) {
            var state = this.STATE_INCORRECT;
            if(!this.isActivity) {
                return this.STATE_NOT_ACTIVITY;
            }

            $.each(this.expressions, function(idx, val) {
                if(val.id == pid) {
                    state = val.variables[variable].value == val.variables[variable].expectedValue ? plot.STATE_CORRECT : plot.STATE_INCORRECT;
                    return false;
                }
            });

            return state;
        }

        this.areCorrectPlotVariables = function(pid) {
            if(!this.isActivity) {
                return this.STATE_NOT_ACTIVITY;
            }

            var state = this.STATE_CORRECT;
            $.each(this.expressions, function(idx, val) {
                if(val.id == pid) {
                    $.each(val, function(vidx, variableObj) {
                        if(variableObj.isExercise &amp;&amp; variableObj.value != variableObj.expectedValue) {
                            state = plot.STATE_INCORRECT;
                            return false;
                        }
                    })
                }
            })

            return state;
        }

        this.isAttempted = function () {
            return this.attempted;
        }

        this.setAttempted = function (state) {
            this.attempted = state;
        }
    }

    var presenter = function(){};
    var addonID;
    var plot = new Plot();
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.GSACounter = 0;
    presenter.ptpShowAnswersPoints = [];
    presenter.isPtpShowAnswersActive = false;

    presenter.ERROR_CODES = {
        'MARK_01': "Expression %expression% is invalid: mark at length must be empty or a positive number between 0 and 100",
    };

    presenter.showErrorMessage = function(message, substitutions) {
        let errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            let messageSubst = message;
            for (let key in substitutions) {
                if (!substitutions.hasOwnProperty(key)) continue;
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        $(presenter.view).html(errorContainer);
    };

    function getErrorObject (errorCode, errorMessageSubstitutions) {
        return {
            isValid: false,
            errorCode: errorCode,
            errorMessageSubstitutions: errorMessageSubstitutions,
        };
    }

    function getCorrectObject (value) {
        return {
            isValid: true,
            value: value
        };
    }

    presenter.setWorkMode = function(){
        var ref;
        if(!presenter.isActivity) {
            return;
        }

        presenter.errorsMode = false;
        $.each(plot.expressions, function(idx, val) {
            ref = plot.svgDoc.find('.is_plot[uid="'+idx+'"]');
            if(ref.length &gt; 0) {
                ref.removeClass('draw_mark_error draw_'+(parseInt(idx)+1)+'_mark_error draw_mark_correct draw_'+(parseInt(idx)+1)+'_mark_correct');
                ref.attr('style', ref.data().cssStyle);
                plot.svgDoc.find('.draw_outline_base[ouid="'+idx+'"]').removeClass('draw_outline_mark_error draw_'+(parseInt(idx)+1)+'_outline_mark_error draw_outline_mark_correct draw_'+(parseInt(idx)+1)+'_outline_mark_correct');
            }
        });
        this.removePointsStateMarks();
        plot.removeMarks();
        plot.enableUI(true);
    };

    presenter.removePointsStateMarks = function() {
        plot.svgDoc.find('.point_error').removeClass('point_error');
        plot.svgDoc.find('.point_outline_mark_error').removeClass('point_outline_mark_error');
        plot.svgDoc.find('.point_correct').removeClass('point_correct');
        plot.svgDoc.find('.point_outline_mark_correct').removeClass('point_outline_mark_correct');
        plot.svgDoc.find('.point_outline_base').removeClass('plot_show_answers');
    };

    presenter.markPointAsError = function(x, y) {
        plot.svgDoc.find('.point[vx="' + x + '"][vy="' + y + '"]').addClass('point_error');
        plot.svgDoc.find('.point_outline_base[vx="' + x + '"][vy="' + y + '"]').addClass('point_outline_mark_error');
    };

    presenter.markPointAsCorrect = function(x, y) {
        plot.svgDoc.find('.point[vx="' + x + '"][vy="' + y + '"]').addClass('point_correct');
        plot.svgDoc.find('.point_outline_base[vx="' + x + '"][vy="' + y + '"]').addClass('point_outline_mark_correct');
    };

    presenter.setPointShowAnswersClass = function(x, y) {
        plot.svgDoc.find('.point_outline_base[vx="' + x + '"][vy="' + y + '"]').addClass('plot_show_answers');
    };

    presenter.onEventReceived = function (eventName, eventData) {
         switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.showAnswers = function() {
        if (!presenter.isActivity) {
            return;
        }

        presenter.errorsMode &amp;&amp; presenter.setWorkMode();
        presenter.handleDisplayedAnswers();
        presenter.isShowAnswersActive = true;

        saveAndClearPlot();
        showAllExpressionsAnswers();
        showAllPointsAnswers();

        plot.enableUI(false);
    };

    presenter.gradualShowAnswers = function(eventData) {
        if (!presenter.isActivity || eventData.moduleID !== addonID) {
            return;
        }
        const answerIndex = parseInt(eventData.item, 10);
        gradualShowAnswers(answerIndex);
    };

    function gradualShowAnswers(answerIndex, includePrevious = false) {
        const isFirstGSA = !presenter.isGradualShowAnswersActive;
        presenter.errorsMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive = true;
        presenter.GSACounter = answerIndex;

        isFirstGSA &amp;&amp; saveAndClearPlot();
        const numberOfCorrectExpressions = plot.expressions.filter(val =&gt; val.correctAnswer).length;
        if (answerIndex &lt; numberOfCorrectExpressions) {
            showExpressionAnswer(answerIndex, includePrevious);
        } else {
            includePrevious &amp;&amp; showExpressionAnswer(numberOfCorrectExpressions - 1, includePrevious);
            showPointAnswer(answerIndex - numberOfCorrectExpressions, includePrevious);
        }

        plot.enableUI(false);
    }

    function saveAndClearPlot() {
        // save status of selected points
        presenter.clickedPoints = {};
        $.extend( true, presenter.clickedPoints, plot.selectedPoints );

        // clear plot from selected points
        $.each(presenter.clickedPoints, function(_, point) {
            plot._deselectPoint(point.x, point.y, true);
        });
        presenter.removePointsStateMarks();

        // clear plot from expressions
        $.each(plot.expressions, function(idx, val) {
            plot.removePlot(idx);
        });
    }

    presenter.setPtpShowAnswersPoints = function(points) {
        presenter.ptpShowAnswersPoints = points;
    }

    presenter.ptpShowAnswers = function(item) {
        presenter.handleDisplayedAnswers();
        presenter.isPtpShowAnswersActive = true;
        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
        presenter.GSACounter = item;
        presenter.saveAndClearPoints();

        for (var i = 0; i &lt; presenter.ptpShowAnswersPoints.length &amp;&amp; i &lt;= item; i++) {
            var point = presenter.ptpShowAnswersPoints[i];
            presenter.setPointShowAnswersClass(point.x, point.y);
        }

        presenter.enableUI(false);
    }

    presenter.saveAndClearPoints = function() {
        // save status of selected points
        presenter.clickedPoints = {};
        $.extend( true, presenter.clickedPoints, plot.selectedPoints );

        // clear plot from selected points
        $.each(presenter.clickedPoints, function(_, point) {
            plot._deselectPoint(point.x, point.y, true);
        });
        presenter.removePointsStateMarks();
    }

    function showAllExpressionsAnswers() {
        plot.expressions.forEach((expression, index) =&gt; {
            expression.correctAnswer &amp;&amp; showExpressionAsAnswer(index);
        })
    }

    function showExpressionAnswer(answerIndex, includePrevious = false) {
        let currentCorrectAnswerIndex = 0;
        for (let i = 0; i &lt; plot.expressions.length; i++) {
            if (!plot.expressions[i].correctAnswer) {
                continue;
            }
            if (currentCorrectAnswerIndex === answerIndex) {
                showExpressionAsAnswer(i);
                return;
            }
            includePrevious &amp;&amp; showExpressionAsAnswer(i);
            currentCorrectAnswerIndex++;
        }
    }

    function showExpressionAsAnswer (expressionIndex) {
        plot.drawPlot(expressionIndex);

        const $refObj = plot.svgDoc.find('.is_plot[uid="'+expressionIndex+'"]');
        $refObj.addClass('show_answers');
        $refObj.removeAttr('style');
        $refObj.removeClass('draw_selected');

        const $refObjOutline = plot.svgDoc.find('.draw_outline_base[ouid="'+expressionIndex+'"]');
        $refObjOutline.addClass('show_answers');
        $refObjOutline.removeClass('draw_outline');
        $refObjOutline.removeClass('draw_"' + expressionIndex + '"_outline');
    }

    function showAllPointsAnswers() {
        plot.points.forEach((point, index) =&gt; {
            point.correct &amp;&amp; showPointAsAnswer(index);
        })
    }

    function showPointAnswer(answerIndex, includePrevious = false) {
        let currentCorrectAnswerIndex = 0;
        for (let i = 0; i &lt; plot.points.length; i++) {
            if (!plot.points[i].correct) {
                continue;
            }
            if (currentCorrectAnswerIndex === answerIndex) {
                showPointAsAnswer(i);
                return;
            }
            includePrevious &amp;&amp; showPointAsAnswer(i);
            currentCorrectAnswerIndex++;
        }
    }

    function showPointAsAnswer(pointIndex) {
        const point = plot.points[pointIndex];
        presenter.setPointShowAnswersClass(point.x, point.y);
    }

    presenter.hideAnswers = function() {
        if (!presenter.isActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.isShowAnswersActive = false;
        _hideAnswers();
    };

    presenter.gradualHideAnswers = function() {
        if (!presenter.isActivity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.isGradualShowAnswersActive = false;
        presenter.GSACounter = 0;
        _hideAnswers();
    };

    presenter.ptpHideAnswers = function() {
        if (!presenter.isPtpShowAnswersActive) return;

        presenter.isPtpShowAnswersActive = false;
        presenter.GSACounter = 0;
        _hideAnswers();
    };

    function _hideAnswers() {
        // POINTS
        presenter.removePointsStateMarks();

        $.each(presenter.clickedPoints, function(_, point) {
            plot._selectPoint(point.x, point.y, true);
        });

        //FUNCTIONS
        $.each(plot.expressions, function(idx, val) {
            if(val.correctAnswer) {
                plot.removePlot(idx);
            }
        });

        plot.drawPlots();

        plot.enableUI(true);
    }

    presenter.setShowErrorsMode = function() {
        if(!presenter.isActivity) {
            return;
        }

        presenter.handleDisplayedAnswers();

        var res;
        presenter.errorsMode = true;

        plot.areIndividualMarks() &amp;&amp; plot.createIndividualMarksSVGDOMGroup();
        plot.enableUI(false);

        $.each(plot.expressions, function(idx, val) {
            if(val.selectable &amp;&amp; val.touched) {
                if(val.correctAnswer == false &amp;&amp; val.selected == true) {
                    //mark error
                    plot.svgDoc.find('.is_plot[uid="'+idx+'"]').addClass('draw_mark_error draw_'+(parseInt(idx)+1)+'_mark_error').removeAttr('style');
                    plot.svgDoc.find('.draw_outline_base[ouid="'+idx+'"]').addClass('draw_outline_mark_error draw_'+(parseInt(idx)+1)+'_outline_mark_error');
                    plot.areIndividualMarks() &amp;&amp; plot.drawErrorMarkForExpression(idx);
                } else if(val.correctAnswer == true &amp;&amp; val.selected == true) {
                    //mark as correct only plots which are selectable, selected, touched and it\'s correct answer is true (selected)
                    plot.svgDoc.find('.is_plot[uid="'+idx+'"]').addClass('draw_mark_correct draw_'+(parseInt(idx)+1)+'_mark_correct').removeAttr('style');
                    plot.svgDoc.find('.draw_outline_base[ouid="'+idx+'"]').addClass('draw_outline_mark_correct draw_'+(parseInt(idx)+1)+'_outline_mark_correct');
                    plot.areIndividualMarks() &amp;&amp; plot.drawCorrectMarkForExpression(idx);
                }
            }
            res = plot.plotVariablesResult(idx);
            if(res.todo != res.done &amp;&amp; res.todo &gt; 0 &amp;&amp; res.touched) {
                //mark error
                plot.svgDoc.find('.is_plot[uid="'+idx+'"]').addClass('draw_mark_error draw_'+(parseInt(idx)+1)+'_mark_error').removeAttr('style');
                plot.svgDoc.find('.draw_outline_base[ouid="'+idx+'"]').addClass('draw_outline_mark_error draw_'+(parseInt(idx)+1)+'_outline_mark_error');
                plot.areIndividualMarks() &amp;&amp; plot.drawErrorMarkForExpression(idx);
            } else if(res.todo == res.done &amp;&amp; res.todo &gt; 0 &amp;&amp; res.touched) {
                //mark correct
                plot.svgDoc.find('.is_plot[uid="'+idx+'"]').addClass('draw_mark_correct draw_'+(parseInt(idx)+1)+'_mark_correct').removeAttr('style');
                plot.svgDoc.find('.draw_outline_base[ouid="'+idx+'"]').addClass('draw_outline_mark_correct draw_'+(parseInt(idx)+1)+'_outline_mark_correct');
                plot.areIndividualMarks() &amp;&amp; plot.drawCorrectMarkForExpression(idx);
            }
        });

        if(plot.points.length &gt; 0) {
            $.each(plot.points, function(idx, val) {
                res = presenter.grepPoints(plot.selectedPoints, val);
                if(val.notScored === false) {
                    if(!val.correct &amp;&amp; res &amp;&amp; val.touched) {
                        //mark wrong
                        presenter.markPointAsError(val.x, val.y);
                        plot.areIndividualMarks() &amp;&amp; plot.drawErrorMarkForPoint(val.x, val.y);
                    } else if(val.correct &amp;&amp; res &amp;&amp; val.touched) {
                        //mark correct
                        presenter.markPointAsCorrect(val.x, val.y);
                        plot.areIndividualMarks() &amp;&amp; plot.drawCorrectMarkForPoint(val.x, val.y);
                    }
                }
            });

            //check excess points
            $.each(plot.selectedPoints, function(idx, val) {
                res = presenter.grepPoints(plot.points, val);
                if(!res) {
                    presenter.markPointAsError(val.x, val.y);
                    plot.areIndividualMarks() &amp;&amp; plot.drawErrorMarkForPoint(val.x, val.y);
                }
            });
        }

        if (plot.isOneMark()) {
            if (presenter.getErrorCount() &gt; 0) {
                plot.drawOneErrorMark();
            } else if (presenter.getMaxScore() === presenter.getScore()) {
                plot.drawOneCorrectMark();
            }
        }
    };

    presenter.getMaxScore = function(){
        if(!presenter.isActivity) {
            return 0;
        }

        var todo = 0;

        $.each(plot.expressions, function(idx, val) {
            //include selectable plots
            if(val.selectable &amp;&amp; val.correctAnswer) {
                todo++;
            }
            //include expected variables
            var res = plot.plotVariablesResult(idx);
            if(res.todo &gt; 0) {
                todo++;
            }
        });
        //include all scorable points
        $.each(plot.points, function(idx, val) {
            if(val.notScored === false) {
                todo++;
            }
        })

        return todo;
    }

    presenter.getScore = function(){
        if(!presenter.isActivity) {
            return 0;
        }

        var done = 0;
        var res;

        $.each(plot.expressions, function(idx, val) {
            if(val.selectable &amp;&amp; val.correctAnswer == val.selected &amp;&amp; val.correctAnswer) {
                done++;
            }
            //get variables score
            res = plot.plotVariablesResult(idx);
            if(res.todo &gt; 0 &amp;&amp; res.todo == res.done) {
                done++;
            }
        });

        $.each(plot.points, function(idx, val) {
            res = presenter.grepPoints(plot.selectedPoints, val);
            if(((val.correct &amp;&amp; res) || (!val.correct &amp;&amp; !res)) &amp;&amp; val.notScored === false) {
                done++;
            }
        })

        return done;
    }

    presenter.grepPoints = function(arr, point) {
        var res = false;
        $.each(arr, function(idx, el) {
            if(el.x == point.x &amp;&amp; el.y == point.y) {
                res = true;
                return;
            }
        });

        return res;
    }

    presenter.getErrorCount = function(){
        if(!presenter.isActivity) {
            return 0;
        }

        var errors = 0;
        var res;
        $.each(plot.expressions, function(idx, val) {
            if(val.selectable &amp;&amp; val.correctAnswer == false &amp;&amp; val.selected == true &amp;&amp; val.touched) {
                errors++;
            }
            //check variables
            res = plot.plotVariablesResult(idx);
            if(res.todo != res.done &amp;&amp; res.touched) {
                errors++;
            }
        })

        //check points
        if(plot.points.length &gt; 0) {
            $.each(plot.points, function(idx, val) {
                res = presenter.grepPoints(plot.selectedPoints, val);
                //var res = $.grep(plot.selectedPoints, function(e){ return e.x == plot.points[p].x &amp;&amp; e.y == plot.points[p].y; });
                if(!val.correct &amp;&amp; res &amp;&amp; val.touched &amp;&amp; val.notScored === false) {
                    errors++;
                }
            })

            $.each(plot.selectedPoints, function(idx, val) {
                res = presenter.grepPoints(plot.points, val);
                //var res = $.grep(plot.points, function(e){ return e.x == plot.selectedPoints[p].x &amp;&amp; e.y == plot.selectedPoints[p].y; });
                if(!res) {
                    errors++;
                }
            });
        }

        return errors;
    }

    presenter.reset = function(){
        presenter.handleDisplayedAnswers();

        presenter.errorsMode = false;
        presenter._allDoneState = false;
        plot.setAttempted(false);
        $.each(plot.expressions, function(idx, val) {
            val.touched = false;
            val.cssColor = val.cssColorInitialValue;
            val.cssProperties = {};
            val.visible = val.initVisible;
            $.each(val.variables, function(vidx, variableObj) {
                variableObj.touched = false;
                variableObj.value = variableObj.initialValue;
            })
        });

        $.each(plot.points, function(idx, val) {
            val.touched = false;
        })
        plot.reset();
        presenter.isVisible = presenter.initIsVisible;
        presenter.updateVisibility();
    };

    presenter.run = function(view, model){
        model = presenter.upgradeModel(model);
        presenter.errorsMode = false;
        presenter.view = view;
        presenter.model = model;
        presenter._allDoneState = false;

        addonID = model.ID;

        presenter.initialize(presenter.view, presenter.model, true);

        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        events.forEach((eventName) =&gt; {
            presenter.eventBus.addEventListener(eventName, this);
        });
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeMarks(model);
        return presenter.upgradeYAxisValuesPosition(upgradedModel);
    };

    presenter.upgradeMarks = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (upgradedModel.hasOwnProperty("Expressions")) {
            upgradedModel.Expressions.map(function (expression) {
                if (!expression["mark at length"]) {
                    expression["mark at length"] = "50";
                }
            });
        }

        if (!upgradedModel.hasOwnProperty("Check marks")) {
            upgradedModel["Check marks"] = "No";
        }

        if (!upgradedModel["Correct marks HTML"]) {
            upgradedModel["Correct marks HTML"] = "&amp;#10004;";
        }

        if (!upgradedModel["Error marks HTML"]) {
            upgradedModel["Error marks HTML"] = "&amp;#10006;";
        }

        return upgradedModel;
    };

    presenter.upgradeYAxisValuesPosition = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty("Y axis values position")) {
            upgradedModel["Y axis values position"] = "";
        }

        return upgradedModel;
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.updateVisibility = function() {
        presenter.isVisible ? presenter.show() : presenter.hide();
    };

    presenter.initialize = function(view, model, isInteractive) {
        var v, p, el;
        this._model = model;
        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        presenter.decimalSeparator = (model['Decimal separator'] === undefined || model['Decimal separator'] == '') ? '.' : model['Decimal separator'];
        if(presenter.decimalSeparator != '.' &amp;&amp; presenter.decimalSeparator != ',') {
            presenter.decimalSeparator = '.';
        };
        if (isInteractive) {
            presenter.updateVisibility();
        } else {
            presenter.show();
        }
        plot.interactive = isInteractive;
        plot.width = parseInt(model['Width']);
        plot.height = parseInt(model['Height']);
        plot.xMin = (model['xMin'] == '' &amp;&amp; model['xMin'] != '0' ? -10 : parseFloat(this.toDotSeparator(model['xMin'])));
        plot.xMax = (model['xMax'] == '' &amp;&amp; model['xMax'] != '0' ? 10 : parseFloat(this.toDotSeparator(model['xMax'])));
        plot.yMin = (model['yMin'] == '' &amp;&amp; model['yMin'] != '0' ? -10 : parseFloat(this.toDotSeparator(model['yMin'])));
        plot.yMax = (model['yMax'] == '' &amp;&amp; model['yMax'] != '0' ? 10 : parseFloat(this.toDotSeparator(model['yMax'])));
        plot.initXMin = plot.xMin;
        plot.initXMax = plot.xMax;
        plot.initYMin = plot.yMin;
        plot.initYMax = plot.yMax;
        plot.grid = model['Grid'].toLowerCase() === 'true' ? true : false;
        plot.gridStepX = Math.abs(parseFloat(this.toDotSeparator(model['GridStepX']))) || 1;
        plot.gridStepY = Math.abs(parseFloat(this.toDotSeparator(model['GridStepY']))) || 1;
        plot.arrowheadSize = parseInt(model['Arrowhead size']) || 6;
        plot.asymptoteMinimumDY = this.toDotSeparator(model['Asymptote DY']) || 5;
        plot.axisValues = model['Axis values'].toLowerCase() === 'true' ? true : false;
        plot.xAxisDescription = model['X axis description'] || 'x';
        plot.yAxisDescription = model['Y axis description'] || 'y';
        plot.xAxisVisible = model['hide X axis'] === undefined || model['hide X axis'].toLowerCase() === 'false' || model['hide X axis'] == '' ? true : false;
        plot.yAxisVisible = model['hide Y axis'] === undefined || model['hide Y axis'].toLowerCase() === 'false' || model['hide Y axis'] == '' ? true : false;
        plot.xAxisValuesPosition = model['X axis values position'];
        plot.yAxisValuesPosition = model['Y axis values position'];
        var xAxisValues = model['Axis x values'] === undefined || model['Axis x values'] == '' || this._hasIllegalCharacters(model['Axis x values'].toString()) ? false : model['Axis x values'].toString().split(this.getSeparatorByDecimalSeparator());
        var yAxisValues = model['Axis y values'] === undefined || model['Axis y values'] == '' || this._hasIllegalCharacters(model['Axis y values'].toString()) ? false : model['Axis y values'].toString().split(this.getSeparatorByDecimalSeparator());
        if(xAxisValues !== false &amp;&amp; xAxisValues.length &gt; 0) {
            $.each(xAxisValues, function(idx, val) {
                if(val.toString().match(/\*/) !== null) {
                    plot.xAxisCyclicValues.push(presenter.toDotSeparator(parseInt(val)));
                } else {
                    plot.xAxisCustomValues.push(parseFloat(presenter.toDotSeparator(val)));
                }
            });
        }
        if(yAxisValues !== false &amp;&amp; yAxisValues.length &gt; 0) {
            $.each(yAxisValues, function(idx, val) {
                if(val.toString().match(/\*/) !== null) {
                    plot.yAxisCyclicValues.push(presenter.toDotSeparator(parseInt(val)));
                } else {
                    plot.yAxisCustomValues.push(parseFloat(presenter.toDotSeparator(val)));
                }
            });
        }
        plot.expressions = new Array();
        plot.pointActiveArea = parseInt(this.toDotSeparator(model['Point active area size'])) || 10;
        plot.pointRadius = parseInt(this.toDotSeparator(model['Point radius'])) || 3;
        plot.pointOutlineRadius = parseInt(this.toDotSeparator(model['Point outline radius'])) || 7;

        for (p in model['Expressions']) {
            if(model['Expressions'][p]['expression'] != '') {
                el = {
                    id: model['Expressions'][p]['id'] === undefined || model['Expressions'][p]['id'] == '' ? p : model['Expressions'][p]['id'],
                    expression: this.toDotSeparator(model['Expressions'][p]['expression']),
                    selectable: model['Expressions'][p]['selectable'].toLowerCase() === 'true' ? true : false,
                    selected: false,
                    correctAnswer: model['Expressions'][p]['correct'].toLowerCase() === 'true' ? true : false,
                    xMin: model['Expressions'][p]['xMin'] === undefined || (model['Expressions'][p]['xMin'] == '' &amp;&amp; model['Expressions'][p]['xMin'] != '0') ? false : this.toDotSeparator(model['Expressions'][p]['xMin']),
                    xMax: model['Expressions'][p]['xMax'] === undefined || (model['Expressions'][p]['xMax'] == '' &amp;&amp; model['Expressions'][p]['xMax'] != '0') ? false : this.toDotSeparator(model['Expressions'][p]['xMax']),
                    yMin: model['Expressions'][p]['yMin'] === undefined || (model['Expressions'][p]['yMin'] == '' &amp;&amp; model['Expressions'][p]['yMin'] != '0') ? false : this.toDotSeparator(model['Expressions'][p]['yMin']),
                    yMax: model['Expressions'][p]['yMax'] === undefined || (model['Expressions'][p]['yMax'] == '' &amp;&amp; model['Expressions'][p]['yMax'] != '0') ? false : this.toDotSeparator(model['Expressions'][p]['yMax']),
                    variables: {},
                    initVisible: model['Expressions'][p]['hidden'] !== undefined &amp;&amp; model['Expressions'][p]['hidden'] != '' &amp;&amp; model['Expressions'][p]['hidden'].toLowerCase() === 'true' ? false : true,
                    type: model['Expressions'][p]['y to x'] === undefined || model['Expressions'][p]['y to x'] == '' || model['Expressions'][p]['y to x'].toLowerCase() === 'false' ? plot.TYPE_X_TO_Y : plot.TYPE_Y_TO_X,
                    touched: false,
                    cssColorInitialValue: model['Expressions'][p]['color'] === undefined || model['Expressions'][p]['color'] == '' ? '' : model['Expressions'][p]['color'],
                    cssProperties: {}
                };
                el.cssColor = el.cssColorInitialValue;
                el.visible = el.initVisible;

                const validatedMarkAtLength = presenter.validateMarkAtLength(el.id, model["Expressions"][p]["mark at length"]);
                if (!validatedMarkAtLength.isValid) {
                    presenter.showErrorMessage(presenter.ERROR_CODES[validatedMarkAtLength.errorCode], validatedMarkAtLength.errorMessageSubstitutions);
                    return;
                }
                el.markAtLength = validatedMarkAtLength.value;
                plot.expressions.push(el);
            }
        }
        for(v in model['Variables']) {
            var plotId = model['Variables'][v]['plot id'];
            var variable = model['Variables'][v]['variable'];
            var variableValue = this.toDotSeparator(model['Variables'][v]['value']);
            var expectedValue = model['Variables'][v]['expected'] === undefined || (model['Variables'][v]['expected'] == '' &amp;&amp; model['Variables'][v]['expected'] != '0') ? null : this.toDotSeparator(model['Variables'][v]['expected']);

            if(plotId != '' &amp;&amp; variable != '') {
                for (var ex in plot.expressions) {
                    if(plot.expressions[ex].id == plotId) {
                        var iv = variableValue != '' ? parseFloat(variableValue) : 0;
                        el = {
                            initialValue: this.valueToFloat(variableValue),
                            value: this.valueToFloat(variableValue),
                            isExercise: expectedValue ? true : false,
                            expectedValue: parseFloat(expectedValue),
                            touched: false
                        };
                        plot.expressions[ex].variables[variable] = el;
                    }
                }
            }
        }

        plot.points = new Array();
        for (p in model['Points']) {
            if((model['Points'][p]['x value'] == '0' || model['Points'][p]['y value'] == '0') || (model['Points'][p]['x value'] != '' &amp;&amp; model['Points'][p]['y value'] != '')) {
                var point = {
                    x: this.toDotSeparator(model['Points'][p]['x value']),
                    y: this.toDotSeparator(model['Points'][p]['y value']),
                    initiallySelected: model['Points'][p]['selected'] === undefined || model['Points'][p]['selected'].toLowerCase() === 'false' ? false : true,
                    correct: model['Points'][p]['correct'] === undefined || model['Points'][p]['correct'].toLowerCase() === 'true' ? true : false,
                    touched: false,
                    notScored: model['Points'][p]['not scored'] === undefined || model['Points'][p]['not scored'].toLowerCase() === 'false' || model['Points'][p]['not scored'] == '' ? false : true,
                    clickable: true
                }
                //if we don't want that point in score make it non-clickable
                if(point.notScored) {
                    point.clickable = false;
                }
                plot.points.push(point);
                if(point.initiallySelected) {
                    plot.selectedPoints.push({
                        x: point.x,
                        y: point.y,
                        clickable: point.clickable
                    });
                }
            }
        }
        plot.maxSelectedPoints = model['Max selected points'] === undefined || model['Max selected points'] == '' || model['Max selected points'] == 0 ? 0 : parseInt(model['Max selected points']);
        presenter.isActivity = model['Not activity'] !== undefined &amp;&amp; model['Not activity'] != '' &amp;&amp; model['Not activity'].toLowerCase() === 'true' ? false : true;
        plot.isActivity = presenter.isActivity;
        plot.freePoints = model['Free points'] !== undefined &amp;&amp; model['Free points'] != '' &amp;&amp; model['Free points'].toLowerCase() === 'true' ? true : false;

        presenter.broadcast = [];
        if(model['Broadcast'] !== '' &amp;&amp; model['Broadcast'] !== undefined) {
            var broadcasts = model['Broadcast'].split(',');
            $.each(broadcasts, function(idx, val) {
                presenter.broadcast.push(val.trim());
            });
        }
        plot.stateChanged = presenter.stateChanged;
        plot.convertValueToDisplay = presenter.convertValueToDisplay;
        plot.calculatePrecision();

        var canvas = $(view).find('.canvas:first');
        canvas.click(function(e) {
            e.stopPropagation();
        });

        canvas.svg({
            onLoad: presenter.onSvgCreate,
            settings: {
                width: '100%',
                height: '100%',
                preserveAspectRatio: "none"
            }
        });

        if(plot.xAxisValuesPosition != '' &amp;&amp; !isNaN(plot.xAxisValuesPosition)){
            plot.setXAxisValuesPosition(plot.xAxisValuesPosition);
        }

        if(plot.yAxisValuesPosition != '' &amp;&amp; !isNaN(plot.yAxisValuesPosition)){
            plot.setYAxisValuesPosition(plot.yAxisValuesPosition);
        }

        plot.checkMarks = model["Check marks"];
        plot.correctMarksHTML = model["Correct marks HTML"];
        plot.errorMarksHTML = model["Error marks HTML"];
    };

    presenter.toDotSeparator = function(value) {
        return (value + '').replace(this.decimalSeparator, '.');
    };

    presenter.getSeparatorByDecimalSeparator = function() {
        return this.decimalSeparator == '.' ? ',' : ';';
    };

    presenter.convertValueToDisplay = function(value) {
        //set correct decimal separator
        value = presenter.replaceDecimalSeparator(value);
        //change minus to en dash
        value = value.replace(new RegExp('\-', 'g'), '\u2013');
        return value;
    };

    presenter.replaceDecimalSeparator = function(value) {
        return (value + '').replace(new RegExp('\\.', 'g'), presenter.decimalSeparator);
    };

    presenter.getDecimalSeparator = function() {
        return presenter.decimalSeparator;
    };

    presenter.validateMarkAtLength = function (expressionIdx, value) {
        const percentage = presenter.valueToPercentage(value);
        if (isNaN(percentage) || percentage &lt; 0 || percentage &gt; 1) {
            return getErrorObject("MARK_01", {expression: expressionIdx});
        }
        return getCorrectObject(percentage);
    }

    presenter.valueToFloat = function(val) {
        if(val === '' || val === undefined || !this.isCorrectDecimal(val)) {
            return Number.NaN;
        }
         val = this.toDotSeparator(val);
         return parseFloat(val);
    };

    presenter.valueToPercentage = function(value) {
        value = parseFloat(value, 10);
        if (isNaN(value)) {
            return value;
        }
        return value/100;
    };

    presenter._hasIllegalCharacters = function(word) {
        var tmpWord;
        if(this.decimalSeparator == ',') {
            //use ';' for ',' seprator
            tmpWord = word.replace(/[^\*0-9;,-]/g, '');
        } else {
            //use ',' for '.' separator
            tmpWord = word.replace(/[^\*0-9,.-]/g, '');
        }
        return tmpWord != word;
    };

    presenter.isCorrectDecimal = function(nmb) {
        if (nmb === null ||
                ModelValidationUtils.isStringEmpty(nmb) ||
                (this.getDecimalSeparator() === ',' &amp;&amp; nmb.toString().indexOf('.') !== -1) ||
                (this.getDecimalSeparator() === '.' &amp;&amp; nmb.toString().indexOf(',') !== -1)) {
            return false;
        }

        return true;
    };

    presenter.createPreview = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.errorsMode = false;
        presenter.view = view;
        presenter.model = model;
        presenter.initialize(presenter.view, presenter.model, false);
    };

    presenter.onSvgCreate = function(svg) {
        plot.svg = svg;
        plot.svgDoc = $(plot.svg._svg);
        var plotScale = plot.svg.group({
            class_:'scale',
            transform:''
        });
        var plotTranslate = plot.svg.group(plotScale, {
            class_:'translate',
            transform:''
        });
        var plotOverall = plot.svg.group(plotTranslate, {
            class_:'overall'
        });
        plot.svg.group(plotOverall, {
            class_:'grid'
        });
        plot.svg.group(plotOverall, {
            class_:'axis'
        });
        plot.svg.group(plotOverall, {
            class_:'drawings'
        });
        plot.svg.group(plotOverall, {
            class_:'points'
        });
        plot.svg.group(plotOverall, {
            class_:'drawingsAreas'
        });
        plot.svg.group(plotOverall, {
            class_:'pointsAreas'
        });
        plot.setScale();
        plot.setStep();
        plot.translate();
        plot.draw();
        if(!presenter.isVisible) {
            presenter.hide();
        }
    };

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'zoomIn'.toLowerCase():
                plot.zoom(1);
                break;
            case 'zoomOut'.toLowerCase():
                plot.zoom(-1);
                break;
            case 'moveLeft'.toLowerCase():
                plot.move(parseInt(this.toDotSeparator(params)),0);
                break;
            case 'moveRight'.toLowerCase():
                plot.move(parseInt(this.toDotSeparator(params))*(-1),0);
                break;
            case 'moveDown'.toLowerCase():
                plot.move(0, parseInt(this.toDotSeparator(params)));
                break;
            case 'moveUp'.toLowerCase():
                plot.move(0, parseInt(this.toDotSeparator(params))*(-1));
                break;
            case 'getState'.toLowerCase():
                return this.getState();
                break;
            case 'setState'.toLowerCase():
                this.setState(params);
                break;
            case 'setVariable'.toLowerCase():
                plot.setVariable(params[0], params[1], presenter.valueToFloat(params[2]));
                break;
            case 'setVisible'.toLowerCase():
                plot.setVisible(params[0], parseInt(params[1]) == 1 ? true : false);
                break;
            case 'setPointVisibility'.toLowerCase():
                plot.setPointVisibility(this.toDotSeparator(params[0]), this.toDotSeparator(params[1]), parseInt(params[2]) == 1 ? true : false);
                break;
            case 'setPlotStyle'.toLowerCase():
                plot.setPlotStyle(params[0], params[1], params[2], params[3]);
                break;
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'restoreView'.toLowerCase():
                plot.restoreInitialViewPort();
                break;
            case 'isAttempted'.toLowerCase():
                return presenter.isAttempted();
                break;
            case 'setAttempted'.toLocaleLowerCase():
                presenter.setAttempted(params);
                break;
            case 'isAllOK'.toLocaleLowerCase():
                return presenter.isAllOK();
        }
        if(presenter.errorsMode) {
            presenter.setShowErrorsMode();
        }
    };

    presenter.enableUI = function(state) {
        plot.enableUI(state);
    };

    presenter.hide = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        presenter.handleDisplayedAnswers();
        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function(isVisible) {
        $(presenter.view).css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.getState = function() {
        const wasShowAnswersActive = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        const wasPtpShowAnswersActive = presenter.isPtpShowAnswersActive;
        const previousGSACounter = presenter.GSACounter;
        presenter.handleDisplayedAnswers();

        var plotState = [];
        var variableState = [];
        var pointsState = [];

        $.each(plot.expressions, function(idx, val) {
            plotState[idx] = {
                selected: val.selected,
                touched: val.touched,
                visible: val.visible
            }
            //old state
            //plotState[p] = val.selected;

            $.each(val.variables, function(vidx, variableObj) {
                variableState.push({
                    plotId: val.id,
                    variable: vidx,
                    value: variableObj.value,
                    touched: variableObj.touched
                });
            });
        })

        $.each(plot.points, function(idx, val) {
            pointsState[idx] = {
                touched: val.touched
            }
        });

        var state = JSON.stringify({
            version: 2,
            plots: plotState,
            variables: variableState,
            selectedPoints: plot.selectedPoints,
            points: pointsState,
            isVisible: presenter.isVisible,
            isAttempted: presenter.isAttempted()
        });

        wasShowAnswersActive &amp;&amp; presenter.showAnswers();
        wasGradualShowAnswersActive &amp;&amp; gradualShowAnswers(previousGSACounter, true);
        wasPtpShowAnswersActive &amp;&amp; presenter.ptpShowAnswers(previousGSACounter);
        return state;
    };

    presenter.setState = function(state) {
        if(state != '' &amp;&amp; state !== undefined) {
            state = JSON.parse(state);
            //old state
            if(state.version == undefined) {
                if(state.plots.length &gt; 0) {
                    $.each(state.plots, function(idx, val) {
                        plot.expressions[idx].touched = true;
                        plot.expressions[idx].selected = val;

                    });
                    plot.selectedPoints = state.points;
                }
            } else {
                if(state.plots.length &gt; 0) {
                    $.each(state.plots, function(idx, val) {
                        plot.expressions[idx].selected = val.selected;
                        plot.expressions[idx].touched = val.touched;
                        plot.expressions[idx].visible = val.visible != undefined ? val.visible : true;
                    });
                }
                //restore variables
                if(state.variables.length &gt; 0) {
                    $.each(state.variables, function(sidx, sval) {
                        $.each(plot.expressions, function(pidx, pval) {
                            if(sval.plotId == pval.id) {
                                pval.variables[sval.variable].value = presenter.valueToFloat(sval.value);
                                pval.variables[sval.variable].touched = sval.touched;
                            }
                        });
                    });
                }
                $.each(state.points, function(idx, val) {
                    plot.points[idx].touched = val.touched
                })
                plot.selectedPoints = [];
                $.each(state.selectedPoints, function(k, v) {
                    plot.selectedPoints.push(
                        {
                            x: v.x,
                            y: v.y,
                            clickable: v.clickable != undefined ? v.clickable : true
                        }
                    )
                });
                if(state.isVisible === false) {
                    presenter.hide();
                } else {
                    presenter.show();
                }
                if(state.isAttempted === undefined) {
                    state.isAttempted = true;
                }
                presenter.setAttempted(state.isAttempted);
            }

            plot.draw();
        }
    }

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.stateChanged = function(data) {
        var receiver;
        if(data) {
            //chained events when data is array
            if(!(data instanceof Array)) {
                //become chain with single event
                data = [data];
            }

            //call chained events
            for(var t=0;t&lt;data.length;t++) {
                data[t].source = addonID;
                data[t].item = data[t].item.toString();
                if(data[t].item.substring(0,6) == 'point_') {
                    data[t].item = presenter.replaceDecimalSeparator(data[t].item);
                }
                data[t].value = data[t].value.toString();
                data[t].score = data[t].score === null ? null : data[t].score.toString();
                //broadcast events or send event to bus
                if(presenter.broadcast.length &gt; 0) {
                    $.each(presenter.broadcast, function(idx, broadcastTo) {
                        receiver = presenter.playerController.getModule(broadcastTo);
                        if(receiver) {
                            receiver.onEvent('ValueChanged', data[t]);
                        }
                    });
                } else {
                    presenter.eventBus.sendEvent('ValueChanged', data[t]);
                }
            }
        }

        if(presenter.broadcast.length === 0) {
            if(presenter.isActivity &amp;&amp; presenter.isAllOK()) {
                if(!presenter._allDoneState) {
                    presenter._allDoneState = true;
                    sendAllOKEvent();
                }
            } else {
                presenter._allDoneState = false;
            }
        }
    };

    function sendAllOKEvent () {
        var eventData = {
            'source': addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    }

    presenter.isAttempted = function () {
        presenter.handleDisplayedAnswers();
        return !this.isActivity ? true : this.getPlot().isAttempted();
    };

    presenter.setAttempted = function (state) {
        this.getPlot().setAttempted(state);
    };

    presenter.getPlot = function() {
        return plot;
    };

    presenter.handleDisplayedAnswers = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.isPtpShowAnswersActive &amp;&amp; presenter.ptpHideAnswers();
    };

    presenter.isAnswersDisplayed = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    };

    presenter.getActivitiesCount = function () {
        if (!presenter.isActivity) {
            return 0;
        }
        const correctPointsNumber = plot.points.filter(val =&gt; val.correct).length;
        const correctExpressionsNumber = plot.expressions.filter(val =&gt; val.correctAnswer).length;
        return correctPointsNumber + correctExpressionsNumber;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="PointsLines" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property displayName="Points' coordinates" name="Points" nameLabel="PointsLines_property_points" type="text"/>
		<property displayName="Points' indexes" name="Indexes" nameLabel="PointsLines_property_indexes" type="text"/>
		<property displayName="Starting lines" name="Starting lines" nameLabel="PointsLines_property_starting_lines" type="text"/>
		<property displayName="Answer" name="Lines" nameLabel="PointsLines_property_lines" type="text"/>
		<property name="Is activity" nameLabel="PointsLines_property_is_activity" type="boolean"/>
		<property displayName="Show all answers in gradual show answers mode" name="showAllAnswersInGradualShowAnswersMode" nameLabel="Points_Lines_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
		<property name="Is disabled" nameLabel="PointsLines_property_is_disabled" type="boolean"/>
        <property name="Blocked lines" nameLabel="PointsLines_property_blocked_lines" type="text"/>
        <property name="Block wrong answers" nameLabel="PointsLines_property_block_wrong_answers" type="boolean"/>
        <property displayName="Single Connection" name="Single" nameLabel="PointsLines_property_single_connection" type="boolean"/>
        <property displayName="Alternative texts" name="alternativeTexts" nameLabel="PointsLines_property_alternative_texts" type="text"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="PointsLines_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="PointsLines_property_speech_texts" type="staticlist">
            <property name="Point" nameLabel="PointsLines_property_speech_text_point" type="staticrow">
                <property name="Point" nameLabel="PointsLines_property_speech_text_point" type="string"/>
            </property>
            <property name="Connected" nameLabel="PointsLines_property_speech_text_connected" type="staticrow">
                <property name="Connected" nameLabel="PointsLines_property_speech_text_connected" type="string"/>
            </property>
            <property name="Disconnected" nameLabel="PointsLines_property_speech_text_disconnected" type="staticrow">
                <property name="Disconnected" nameLabel="PointsLines_property_speech_text_disconnected" type="string"/>
            </property>
            <property name="ConnectedTo" nameLabel="PointsLines_property_speech_text_connected_to" type="staticrow">
                <property name="ConnectedTo" nameLabel="PointsLines_property_speech_text_connected_to" type="string"/>
            </property>
            <property name="Selected" nameLabel="PointsLines_property_speech_text_selected" type="staticrow">
                <property name="Selected" nameLabel="PointsLines_property_speech_text_selected" type="string"/>
            </property>
            <property name="Deselected" nameLabel="PointsLines_property_speech_text_deselected" type="staticrow">
                <property name="Deselected" nameLabel="PointsLines_property_speech_text_deselected" type="string"/>
            </property>
            <property name="Correct" nameLabel="PointsLines_property_speech_text_correct" type="staticrow">
                <property name="Correct" nameLabel="PointsLines_property_speech_text_correct" type="string"/>
            </property>
            <property name="Wrong" nameLabel="PointsLines_property_speech_text_wrong" type="staticrow">
                <property name="Wrong" nameLabel="PointsLines_property_speech_text_wrong" type="string"/>
            </property>
        </property>
	</model>
<css>.addon_PointsLines .pointslines {
    border: 3px solid #ccc;
    position:absolute;
    -ms-touch-action: none;
    touch-action: none;
}
.addon_PointsLines .disabled {
    height: 100%;
    width: 100%;
    background-color: gray;
    opacity: 0.3;
    position:absolute;
}

.addon_PointsLines .correct {
    border: 3px solid green;
}

.addon_PointsLines .wrong {
    border: 3px solid red;
}

.addon_PointsLines .point_container {
    position: absolute;
    min-height: 25px;
    min-width: 25px;
}

.addon_PointsLines .point {
    position: relative;
    height: 16px;
    width: 16px;
    background-color: gray;
    border-radius: 8px;
    float: left;
    left:-8px;
    top: -8px;
    margin-bottom: -5px;
}

.addon_PointsLines .selected {
    background-color: blue;
}

.addon_PointsLines .line {
    position: absolute;
    height:3px;
    background-color: blue;
}

.addon_PointsLines .line-show-answer {
    position: absolute;
    height:3px;
    background-color: gray;
}

.addon_PointsLines .noremovable {
    background-color: black;
}

.addon_PointsLines .correctLine {
    background-color: green;
}

.addon_PointsLines .wrongLine {
    background-color: red;
}

.addon_PointsLines .point_index {
    color: black;
    cursor: default;
}
</css><view>&lt;div class="pointslines"&gt;&lt;/div&gt;
</view><preview>&lt;div class="pointslines"&gt;&lt;/div&gt;
</preview><presenter>function AddonPointsLines_create() {
    var presenter = function() {};
    presenter.error = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.answersNumber = 0;

    presenter.altTexts = [];
    presenter.langTag = '';
    presenter.isWCAGOn = false;
    presenter.speechTexts = null;
    presenter.keyboardControllerObject = null;

    presenter.ERROR_CODES = {
        'PE' : 'Points coordinates incorrect!',
        'PO' : 'Points outside the addon!',
        'NP' : 'Define the points!',
        'IE' : 'Indexes incorrect!',
        'LE' : 'Starting lines incorrect!',
        'BL' : 'Blocked lines incorrect!',
        'AE' : 'Answer incorrect!',
        'AT' : 'Alternative texts incorrect!'
    };

    presenter.DEFAULT_TTS_PHRASES = {
        POINT: "Point",
        CONNECTED: "Connected",
        DISCONNECTED: "Disconnected",
        CONNECTED_TO: "Connected to",
        SELECTED: "Selected",
        DESELECTED: "Deselected",
        WRONG: "Wrong",
        CORRECT: "Correct",
    };

    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'isAllOK'.toLowerCase():
                presenter.isAllOK();
                break;
            case 'markAsCorrect'.toLowerCase():
                presenter.markAsCorrect();
                break;
            case 'markAsWrong'.toLowerCase():
                presenter.markAsWrong();
                break;
            case 'markAsNeutral'.toLowerCase():
                presenter.markAsNeutral();
                break;
            case 'isEmpty'.toLowerCase():
                presenter.isEmpty();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'isConnected'.toLowerCase():
                presenter.isConnected(params[0],params[1]);
                break;
        }
    };

    presenter.isAttempted = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        return (!(presenter.activity) || (presenter.getScore() !== 0) || (presenter.getErrorCount() !== 0));
    };

    presenter.markAsCorrect = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.$view.find('.pointslines').removeClass('wrong');
        presenter.$view.find('.pointslines').addClass('correct');
    };

    presenter.markAsWrong = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.$view.find('.pointslines').removeClass('correct');
        presenter.$view.find('.pointslines').addClass('wrong');
    };

    presenter.markAsNeutral = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.$view.find('.pointslines').removeClass('correct');
        presenter.$view.find('.pointslines').removeClass('wrong');
    };

    presenter.isConnected = function(i,j) {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        var lineIndex = presenter.currentLines[Math.min(i-1,j-1)][Math.max(i-1,j-1)];
        if (lineIndex == 1 || lineIndex == 2) {
            return true;
        } else {
            return false;
        }
    };

    presenter.isEmpty = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        var numberOfPoints = presenter.points.length;
        var i, j;
        for (i = 0; i &lt; numberOfPoints; i++) {
            for (j = i; j &lt; numberOfPoints; j++) {
                if (presenter.startingLines[i][j] !== presenter.currentLines[i][j]) {
                    return false;
                }
            }
        }
        return true;
    };

    presenter.isAllOK = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        return ((presenter.getScore() == presenter.getMaxScore()) &amp;&amp; (presenter.getErrorCount() === 0));
    };

    presenter.disable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        if (!(presenter.$view.find('.disabled').length &gt; 0)) {
            presenter.disabled = true;
            div = $('&lt;div&gt;');
            div.attr('id', 'disabled_' + presenter.addonID);
            div.attr('class', 'disabled');
            presenter.$view.find('.pointslines').append(div);
        }
    };

    presenter.enable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.disabled = false;
        presenter.$view.find('.disabled').remove();
    };

    presenter.hide = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.isVisible = false;
        presenter.setVisibility(false);
    };

    presenter.show = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.isVisible = true;
        presenter.setVisibility(true);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.updateVisibility = function() {
        if (presenter.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };
    
    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        const events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (const event of events) {
            presenter.eventBus.addEventListener(event, this);
        }
    };
    presenter.createEventData = function(line, state, score) {
        return {
            source : presenter.addonID,
            item : line,
            value : state,
            score : score
        };
    };

    function uncheckLine(line) {
        var splittedLine = line.split("_"),
            point1 = splittedLine[1],
            point2 = splittedLine[2];

        presenter.$view.find('#line_'+(point1)+'_'+(point2)).remove();
        presenter.currentLines[point1][point2] = 0;

        presenter.$view.find('#point_'+presenter.addonID+'_'+presenter.selectedPoint).removeClass('selected');
        presenter.selectedPoint = -1;
    }

    presenter.triggerLineEvent = function(line, state, score) {
        var eventData = presenter.createEventData(line, state, score);
        if(parseInt(score, 10) === 0 &amp;&amp; presenter.blockWrongAnswers) {
            uncheckLine(line);
            if(presenter.singleMode) {
                var splittedLine = line.split("_"),
                    point1 = splittedLine[1],
                    point2 = splittedLine[2];
                presenter.pointsConnected[point1] = 0;
                presenter.pointsConnected[point2] = 0;
            }
        }
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    function checkIndexes(indexes, numberOfPoints) {
        indexes = indexes.replace(/\s/g, '');
        var pointsIndexes = new Array(numberOfPoints);
        var i;
        if (indexes == '') {
            for (i = 0; i &lt; numberOfPoints; i++) {
                pointsIndexes[i] = '';
            }
            return pointsIndexes;
        } else {
            pointsIndexes = indexes.split(',');
            if (pointsIndexes.length != numberOfPoints) {
                presenter.error = 'IE';
                return false;
            } else {
                return pointsIndexes;
            }
        }
    }

    function getPoint(con, coords) {
        coords = coords.replace(/\s/g, '');
        if (coords == '') {
            presenter.error = 'NP';
            return false;
        } else {
            var points = coords.split(']');
            var numberOfPoints = points.length - 1;
            var pointsCoordinates = new Array(numberOfPoints);
            var tmp_dane;
            if (points[numberOfPoints] != '') {
                presenter.error = 'PE';
                return false;
            }
            var i;
            for (i = 0; i &lt; points.length - 1; i++) {
                tmp_dane = points[i].split(',');
                pointsCoordinates[i] = new Array(2);
                if (tmp_dane[0][0] != '[') {
                    presenter.error = 'PE';
                    return false;
                }
                pointsCoordinates[i][0] = tmp_dane[0].substring(1);
                pointsCoordinates[i][1] = tmp_dane[1];
                if (isNaN(pointsCoordinates[i][0]) || isNaN(pointsCoordinates[i][1])) {
                    presenter.error = 'PE';
                    return false;
                } else if (pointsCoordinates[i][0] &gt;= (con.width()) || pointsCoordinates[i][0] &lt;= 0 || pointsCoordinates[i][1] &lt;= 0 || pointsCoordinates[i][1] &gt;= (con.height())) {
                    presenter.error = 'PO';
                    return false;
                }
                pointsCoordinates[i][1] = parseInt(pointsCoordinates[i][1], 10);
                pointsCoordinates[i][0] = parseInt(pointsCoordinates[i][0], 10);
            }
        }
        return pointsCoordinates;
    }

    function getLines(dataLines, numberOfPoints, type) {
        if (dataLines == undefined) dataLines='';
        dataLines = dataLines.replace(/\s/g, '');
        var Lines = new Array(numberOfPoints);
        var i, j, tmp_dane, point1, point2;

        for (i = 0; i &lt; numberOfPoints; i++) {
            Lines[i] = new Array(numberOfPoints);
            for (j = i; j &lt; numberOfPoints; j++) {
                Lines[i][j] = 0;
            }
        }

        if (dataLines == '') {
            return Lines;
        }
        var addLine = dataLines.split(',');
        for (i = 0; i &lt; addLine.length; i++) {
            tmp_dane = addLine[i].split('-');
            if (tmp_dane[0] == '' || tmp_dane[1] == '') {
                presenter.error = 'LE';
                return false;
            }
            if (!isNaN(tmp_dane[0]) &amp;&amp; tmp_dane[0] &lt;= numberOfPoints &amp;&amp; (!isNaN(tmp_dane[1])) &amp;&amp; tmp_dane[1] &lt;= numberOfPoints) {
                point1 = Math.min(parseInt(tmp_dane[1],10)-1,parseInt(tmp_dane[0],10)-1);
                point2 = Math.max(parseInt(tmp_dane[1],10)-1,parseInt(tmp_dane[0],10)-1);
                Lines[point1][point2] = 1;
            } else if (type == 1 &amp;&amp; !isNaN(tmp_dane[0]) &amp;&amp; parseInt(tmp_dane[0], 10) &lt;= numberOfPoints &amp;&amp; !isNaN(tmp_dane[1].substring(0, tmp_dane[1].length - 1)) &amp;&amp; (tmp_dane[1].substring(tmp_dane[1].length - 1)) === '*' &amp;&amp; parseInt(tmp_dane[1].substring(0, tmp_dane[1].length - 1), 10) &lt;= numberOfPoints) {
                point1 = Math.min(parseInt(tmp_dane[1].substring(0,tmp_dane[1].length-1),10)-1,parseInt(tmp_dane[0],10)-1);
                point2 = Math.max(parseInt(tmp_dane[1].substring(0,tmp_dane[1].length-1),10)-1,parseInt(tmp_dane[0],10)-1);
                Lines[point1][point2] = 2;
            } else {
                if (type == 1) {
                    presenter.error = 'LE';
                } else if (type == 2) {
                    presenter.error = 'AE';
                } else {
                    presenter.error = 'BL';
                }
                return false;
            }
        }
        return Lines;
    }

    function getAlternativeTexts(configurationText) {
        configurationText = window.xssUtils.sanitize(configurationText);
        let texts = configurationText.split(/\r?\n/);
        texts.forEach((text, index) =&gt; {
            texts[index] = text.replace(/\s/g, '');
        })

        if (texts.length &gt; presenter.points.length) {
            presenter.error = 'AT';
            return false;
        }
        return texts;
    }

    presenter.drawPoints = function() {
        for ( var i = 0; i &lt; (presenter.points).length; i++) {
            div = $('&lt;div&gt;');
            div.attr('id', 'point_container_' + presenter.addonID + '_' + i);
            div.attr('class', 'point_container');
            div.attr('order_value', i + 1);
            div.attr('style', 'left: ' + presenter.points[i][0] + 'px; top: ' + presenter.points[i][1] + 'px;');
            presenter.$view.find('.pointslines').append(div);
            div = $('&lt;div class="point"&gt;&lt;/div&gt;');
            div.attr('id', 'point_' + presenter.addonID + '_' + i);
            presenter.$view.find('#point_container_' + presenter.addonID + '_' + i).append(div);
            div = $('&lt;div class="point_index"&gt;' + presenter.indexes[i] + '&lt;/div&gt;');
            presenter.$view.find('#point_container_' + presenter.addonID + '_' + i).append(div);
        }
    };

    presenter.drawLine = function(i, j, showAnswers) {
        if (presenter.$view.find('#line_' + (i) + '_' + (j)).length &lt;= 0 || showAnswers) {
            var m, angle, d, transform, id, line;
            x1 = parseInt(presenter.points[i][0], 10);
            y1 = parseInt(presenter.points[i][1], 10);
            x2 = parseInt(presenter.points[j][0], 10);
            y2 = parseInt(presenter.points[j][1], 10);
            m = (y2 - y1) / (x2 - x1);
            angle = (Math.atan(m)) * 180 / (Math.PI);
            d = Math.sqrt(((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1)));
            if (x2 &gt;= x1) {
                transform = (360 + angle) % 360;
            } else {
                transform = 180 + angle;
            }

            div = $('&lt;div&gt;');
            if (!showAnswers) {
                id = 'line_' + i + '_' + j;
            } else {
                id = 'line_show_answer_' + i + '_' + j;
            }
            div.attr('id', id);
            div.attr('point1', i);
            div.attr('point2', j);
            if (!showAnswers) {
                div.attr('class', 'line');
            } else {
                div.attr('class', 'line-show-answer');
            }
            div.attr('style', 'left: ' + x1 + 'px; top: ' + y1 + 'px');
            presenter.$view.find('.pointslines').prepend(div);
            presenter.$view.find('#'+id).css({
                'left' : x1,
                'top' : y1,
                'width' : d,
                'transform' : 'rotate(' + transform + 'deg)',
                'transform-origin' : '0px 0px',
                '-ms-transform' : 'rotate(' + transform + 'deg)',
                '-ms-transform-origin' : '0px 0px',
                '-moz-transform' : 'rotate(' + transform + 'deg)',
                '-moz-transform-origin' : '0px 0px',
                '-webkit-transform' : 'rotate(' + transform + 'deg)',
                '-webkit-transform-origin' : '0px 0px',
                '-o-transform' : 'rotate(' + transform + 'deg)',
                '-o-transform-origin' : '0px 0px'
            });
            if (presenter.startingLines[i][j] == 2) {
                presenter.$view.find('#line_' + i + '_' + j).addClass('noremovable');
            }
            if (presenter.keyboardControllerObject) {
                presenter.keyboardControllerObject.readConnected();
            }
        }
    };

    presenter.drawTempLine = function (i, x, y) {
        var scaledPoint = scalePoint({x: x, y: y});
        x = scaledPoint.x;
        y = scaledPoint.y;

        if (presenter.draw !== false) {
            if (presenter.$view.find('#line_tmp').length &gt; 0) {
                presenter.$view.find('#line_tmp').remove();
            }
            var m, angle, d, transform, id, line;
            x1 = parseInt(presenter.points[i][0],10);
            y1 = parseInt(presenter.points[i][1],10);
            m = (y-y1)/(x-x1);
            angle = (Math.atan(m))*180/(Math.PI);
            d = Math.sqrt(((x-x1)*(x-x1)) + ((y-y1)*(y-y1)));
            if (x &gt;= x1){
                transform = (360 + angle) % 360;
            } else {
                transform = 180 + angle;
            }

            div = $('&lt;div&gt;');
            div.attr('id','line_tmp');
            div.attr('class','line');
            div.attr('style','left: '+x1+'px; top: '+y1+'px');
            presenter.$view.find('.pointslines').prepend(div);
            presenter.$view.find('#line_tmp').css({
                'left': x1,
                'top': y1,
                'width': d,
                'transform' : 'rotate('+transform+'deg)',
                'transform-origin' : '0px 0px',
                '-ms-transform' : 'rotate('+transform+'deg)',
                '-ms-transform-origin' : '0px 0px',
                '-moz-transform' : 'rotate('+transform+'deg)',
                '-moz-transform-origin' : '0px 0px',
                '-webkit-transform' : 'rotate('+transform+'deg)',
                '-webkit-transform-origin' : '0px 0px',
                '-o-transform' : 'rotate('+transform+'deg)',
                '-o-transform-origin' : '0px 0px'
            });
        }
    };

    presenter.initiate = function(view, model) {
        presenter.view = view;
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        var coords = presenter.model['Points'];
        presenter.activity = ModelValidationUtils.validateBoolean(presenter.model['Is activity']);
        presenter.disabled = ModelValidationUtils.validateBoolean(presenter.model['Is disabled']);
        presenter.singleMode = ModelValidationUtils.validateBoolean(presenter.model['Single']);
        presenter.langTag = model['langAttribute'];
        presenter.showAllAnswersInGradualShowAnswersMode = ModelValidationUtils.validateBoolean(presenter.model['showAllAnswersInGradualShowAnswersMode']);
        presenter.initDisabled = presenter.disabled;
        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.initIsVisible = presenter.isVisible;
        var startingLines = presenter.model['Starting lines'];
        var blockedLines = presenter.model['Blocked lines'];
        presenter.blockWrongAnswers = ModelValidationUtils.validateBoolean(presenter.model['Block wrong answers']);
        var con = presenter.$view.find('.pointslines').parent();
        presenter.$view.find('.pointslines').css({
            'width' : con.width(),
            'height' : con.height()
        });
        presenter.points = getPoint(con, coords);
        presenter.indexes = checkIndexes(presenter.model['Indexes'],(presenter.points).length);
        presenter.altTexts = getAlternativeTexts(model["alternativeTexts"]);
        if (presenter.points === false || presenter.indexes === false || presenter.altTexts === false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
            return;
        }
        presenter.drawPoints();
        numberOfPoints = presenter.points.length;
        if (presenter.singleMode) presenter.pointsConnected =  new Array(numberOfPoints);
        presenter.startingLines = getLines(startingLines, numberOfPoints, 1);
        if (presenter.startingLines === false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
            return;
        }
        presenter.currentLines = getLines(startingLines, numberOfPoints, 1);
        presenter.blockedLines = getLines(blockedLines, numberOfPoints, 3);
        if (presenter.blockedLines === false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
            return;
        }
        if (presenter.singleMode) {
            for ( var i = 0; i &lt; numberOfPoints; i++) {
                presenter.pointsConnected[i] = 0;
            }
        }
        for ( var i = 0; i &lt; numberOfPoints; i++) {
            for ( var j = i; j &lt; numberOfPoints; j++) {
                if (presenter.currentLines[i][j] == 1 || presenter.currentLines[i][j] == 2) {
                    presenter.drawLine(i, j);
                    if (presenter.singleMode) {
                        presenter.pointsConnected[i] = 1;
                        presenter.pointsConnected[j] = 1;
                    }
                }
            }
        }
        var answer = presenter.model['Lines'];
        presenter.answer = getLines(answer, numberOfPoints, 2);
        if (presenter.answer === false) {
            con.text(presenter.ERROR_CODES[presenter.error]);
            return;
        }
    };

    presenter.doClick = function(i) {
        if (presenter.selectedPoint == i) {
            presenter.$view.find('#point_'+presenter.addonID+'_'+i).removeClass('selected');
            presenter.selectedPoint = -1;
        } else if (presenter.selectedPoint !== -1) {
            point1 = Math.min(parseInt(presenter.selectedPoint,10),parseInt(i,10));
            point2 = Math.max(parseInt(presenter.selectedPoint,10),parseInt(i,10));
            if (presenter.currentLines[point1][point2] === 0 &amp;&amp; presenter.blockedLines[point1][point2] != 1) {
                if(presenter.singleMode &amp;&amp; (presenter.pointsConnected[point1]== 1 || presenter.pointsConnected[point2] == 1)) {
                } else {
                    if (presenter.singleMode) {
                        presenter.pointsConnected[point1] = 1;
                        presenter.pointsConnected[point2] = 1;
                    };
                    presenter.drawLine(point1,point2);
                    presenter.currentLines[point1][point2] = 1;
                    line = 'line_'+(point1)+'_'+(point2);
                    if (presenter.startingLines[point1][point2] === 0 &amp;&amp; presenter.answer[point1][point2] == 1) {
                        score = 1;
                    } else {
                        score = 0;
                    }
                    presenter.triggerLineEvent(line,1,score);
                    line = 'all';
                    score = '';
                    if (presenter.isAllOK() &amp;&amp; presenter.activity) {
                        presenter.triggerLineEvent(line,score,score);
                    }
                    presenter.$view.find('#point_'+presenter.addonID+'_'+presenter.selectedPoint).removeClass('selected');
                    presenter.selectedPoint = -1;
                }
            } else if (presenter.currentLines[point1][point2] == 1) {
                presenter.$view.find('#line_'+(point1)+'_'+(point2)).remove();
                if (presenter.keyboardControllerObject) {
                    presenter.keyboardControllerObject.readDisconnected();
                }
                presenter.currentLines[point1][point2] = 0;
                if (presenter.singleMode) {
                    presenter.pointsConnected[point1] = 0;
                    presenter.pointsConnected[point2] = 0;
                };
                line = 'line_'+(point1)+'_'+(point2);
                if (presenter.startingLines[point1][point2] == 1 &amp;&amp; presenter.answer[point1][point2] === 0) {
                    score = 1;
                } else {
                    score = 0;
                }
                presenter.triggerLineEvent(line,0,score);
                line = 'all';
                score = '';
                if (presenter.isAllOK() &amp;&amp; presenter.activity) {
                    presenter.triggerLineEvent(line,score,score);
                }
                presenter.$view.find('#point_'+presenter.addonID+'_'+presenter.selectedPoint).removeClass('selected');
                presenter.selectedPoint = -1;
            } else {
                presenter.$view.find('#point_'+presenter.addonID+'_'+presenter.selectedPoint).removeClass('selected');
                presenter.selectedPoint = -1;
            }
        } else {
            presenter.$view.find('#point_'+presenter.addonID+'_'+i).addClass('selected');
            presenter.selectedPoint = i;
        }
    };

    presenter.upgradeModel = function(model) {
        let upgradedModel = presenter.upgradeShowAllAnswersInGSAMode(model);
        upgradedModel = presenter.upgradeAlternativeTexts(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeSpeechText(upgradedModel);

        return upgradedModel;
    };

    presenter.upgradeShowAllAnswersInGSAMode = function (model) {
        let upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel.hasOwnProperty('showAllAnswersInGradualShowAnswersMode')) {
            upgradedModel['showAllAnswersInGradualShowAnswersMode'] = false;
        }

        return upgradedModel;
    };

    presenter.upgradeAlternativeTexts = function (model) {
        let upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel.hasOwnProperty('alternativeTexts')) {
            upgradedModel['alternativeTexts'] = '';
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        let upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('langAttribute')) {
            upgradedModel['langAttribute'] = '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechText = function (model) {
        let upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('speechTexts')) {
            upgradedModel['speechTexts'] = {};
        }

        const speechTextProperties = [
            'Point', 'Connected', 'Disconnected', 'ConnectedTo',
            'Selected', 'Deselected', 'Correct', 'Wrong'
        ];
        speechTextProperties.forEach((propertyName) =&gt; {
            if (!upgradedModel['speechTexts'].hasOwnProperty(propertyName)) {
                let property = {};
                property[propertyName] = "";
                upgradedModel['speechTexts'][propertyName] = property;
            }
        });

        return upgradedModel;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            point: presenter.DEFAULT_TTS_PHRASES.POINT,
            connected: presenter.DEFAULT_TTS_PHRASES.CONNECTED,
            disconnected: presenter.DEFAULT_TTS_PHRASES.DISCONNECTED,
            connectedTo: presenter.DEFAULT_TTS_PHRASES.CONNECTED_TO,
            selected: presenter.DEFAULT_TTS_PHRASES.SELECTED,
            deselected: presenter.DEFAULT_TTS_PHRASES.DESELECTED,
            correct: presenter.DEFAULT_TTS_PHRASES.CORRECT,
            wrong: presenter.DEFAULT_TTS_PHRASES.WRONG,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            point: TTSUtils.getSpeechTextProperty(
                speechTexts.Point.Point,
                presenter.speechTexts.point),
            connected: TTSUtils.getSpeechTextProperty(
                speechTexts.Connected.Connected,
                presenter.speechTexts.connected),
            disconnected: TTSUtils.getSpeechTextProperty(
                speechTexts.Disconnected.Disconnected,
                presenter.speechTexts.disconnected),
            connectedTo: TTSUtils.getSpeechTextProperty(
                speechTexts.ConnectedTo.ConnectedTo,
                presenter.speechTexts.connectedTo),
            selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.selected),
            deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.deselected),
            correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.correct),
            wrong: TTSUtils.getSpeechTextProperty(
                speechTexts.Wrong.Wrong,
                presenter.speechTexts.wrong),
        };
    };

    presenter.run = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.initiate(view, model);
        presenter.setSpeechTexts(model["speechTexts"]);
        if (!presenter.error) {
            var $div = presenter.$view.find('.pointslines');
            var Width = $div.width();
            var Height = $div.height();
            var i, j;
            var line, score;
            var point1, point2, distance;
            presenter.selectedPoint = -1;
            presenter.draw = false;
            var timeClick = true;
            if (presenter.disabled) presenter.disable();


            presenter.$view.find('.point_container').on('mousedown', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    presenter.draw = parseInt($(this).attr('order_value'),10)-1;
                }

            });

            presenter.$view.on('mousemove',function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.mouseSX = parseInt(e.pageX,10) - parseInt($div.offset().left,10);
                presenter.mouseSY = parseInt(e.pageY,10) - parseInt($div.offset().top,10);
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    presenter.drawTempLine(presenter.draw,presenter.mouseSX,presenter.mouseSY);
                }
            });

            presenter.$view.find('.point_container').on('mouseup',function(e){
                e.stopPropagation();
                e.preventDefault();
                if (presenter.draw !== false &amp;&amp; !presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    if (presenter.$view.find('#line_tmp').length &gt; 0) {
                        presenter.$view.find('#line_tmp').remove();
                    }
                    j = parseInt($(this).attr('order_value'),10)-1;
                    if (presenter.draw !== j &amp;&amp; presenter.currentLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 0 &amp;&amp; presenter.blockedLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] != 1) {
                        if(presenter.singleMode &amp;&amp; (presenter.pointsConnected[j]== 1 || presenter.pointsConnected[presenter.draw] == 1)) {
                        } else {
                            if(presenter.singleMode) {
                                presenter.pointsConnected[j] = 1;
                                presenter.pointsConnected[presenter.draw] = 1;
                            }
                            presenter.drawLine(Math.min(presenter.draw,j),Math.max(presenter.draw,j));
                            presenter.currentLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] = 1;
                            line = 'line_'+(Math.min(presenter.draw,j))+'_'+(Math.max(presenter.draw,j));
                            if (presenter.selectedPoint !== -1) {
                                presenter.$view.find('#point_'+presenter.addonID+'_'+i).removeClass('selected');
                                presenter.selectedPoint = -1;
                            }
                            if (presenter.startingLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 0 &amp;&amp; presenter.answer[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 1) {
                                score = 1;
                            } else {
                                score = 0;
                            }
                            presenter.triggerLineEvent(line,1,score);
                            line = 'all';
                            score = '';
                            if (presenter.isAllOK() &amp;&amp; presenter.activity) {
                                presenter.triggerLineEvent(line,score,score);
                            }
                        }
                    }
                }
                presenter.draw = false;
            });

            presenter.$view.on('mouseup mouseleave',function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.draw = false;

                if (presenter.$view.find('#line_tmp').length &gt; 0) {
                    presenter.$view.find('#line_tmp').remove();
                }
            });

            presenter.$view.find('.point_container').on('touchstart', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive) {
                    presenter.mouseSX = parseInt(e.originalEvent.touches[0].pageX,10) - parseInt($div.offset().left,10);
                    presenter.mouseSY = parseInt(e.originalEvent.touches[0].pageY,10) - parseInt($div.offset().top,10);
                    presenter.mouseX = parseInt(e.originalEvent.touches[0].pageX,10) - parseInt($div.offset().left,10);
                    presenter.mouseY = parseInt(e.originalEvent.touches[0].pageY,10) - parseInt($div.offset().top,10);
                    presenter.draw = parseInt($(this).attr('order_value'),10)-1;
                }
            });

            presenter.$view.find('.pointslines').on('touchmove', function(e){
                e.stopPropagation();
                e.preventDefault();
                presenter.mouseX = parseInt(e.originalEvent.touches[0].pageX,10) - parseInt($div.offset().left,10);
                presenter.mouseY = parseInt(e.originalEvent.touches[0].pageY,10) - parseInt($div.offset().top,10);
                if (presenter.mouseX &gt;  Width || presenter.mouseY &gt; Height || presenter.mouseX &lt; 0 || presenter.mouseY &lt; 0) {
                    presenter.draw = false;
                    if (presenter.$view.find('#line_tmp').length &gt; 0) {
                        presenter.$view.find('#line_tmp').remove();
                    }
                }
                if (presenter.draw !== false) {
                    presenter.drawTempLine(presenter.draw,presenter.mouseX,presenter.mouseY);
                }
            });

            presenter.$view.on('touchend', function(e){
                e.stopPropagation();
                e.preventDefault();
                if (Math.abs(presenter.mouseSX - presenter.mouseX) + Math.abs(presenter.mouseSY - presenter.mouseY) &lt; 15 &amp;&amp; presenter.draw !== false) {
                    if (timeClick) presenter.doClick(presenter.draw);
                    timeClick = false;
                    setTimeout(function(){timeClick = true;},310);
                    //			presenter.doClick(presenter.draw);
                } else  if (presenter.draw !== false){
                    j = -1;
                    var scaledPoint = scalePoint({x: presenter.mouseX, y: presenter.mouseY});
                    presenter.mouseX = scaledPoint.x;
                    presenter.mouseY = scaledPoint.y;

                    for (i = 0; i &lt; (presenter.points).length; i++) {
                        distance = Math.abs(presenter.mouseX - parseInt(presenter.points[i][0], 10)) + Math.abs(presenter.mouseY - parseInt(presenter.points[i][1], 10));
                        if (distance &lt; 25) {
                            j = i;
                        }
                    }
                    if (j !== -1 &amp;&amp; presenter.draw !== j &amp;&amp; presenter.currentLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 0  &amp;&amp; presenter.blockedLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] != 1) {
                        if(presenter.singleMode &amp;&amp; (presenter.pointsConnected[j]== 1 || presenter.pointsConnected[presenter.draw] == 1)) {
                        } else {
                            if(presenter.singleMode) {
                                presenter.pointsConnected[j] = 1;
                                presenter.pointsConnected[presenter.draw] = 1;
                            }
                            presenter.drawLine(Math.min(presenter.draw,j),Math.max(presenter.draw,j));
                            presenter.currentLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] = 1;
                            line = 'line_'+(Math.min(presenter.draw,j))+'_'+(Math.max(presenter.draw,j));
                            if (presenter.selectedPoint !== -1) {
                                presenter.$view.find('#point_'+presenter.addonID+'_'+presenter.selectedPoint).removeClass('selected');
                                presenter.selectedPoint = -1;
                            }

                            if (presenter.startingLines[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 0 &amp;&amp; presenter.answer[Math.min(presenter.draw,j)][Math.max(presenter.draw,j)] === 1) {
                                score = 1;
                            } else {
                                score = 0;
                            }
                            presenter.triggerLineEvent(line,1,score);
                            if (presenter.isAllOK() &amp;&amp; presenter.activity) {
                                line = 'all';
                                score = '';
                                presenter.triggerLineEvent(line,score,score);
                            }
                        }
                    }
                }
                presenter.draw = false;
                if (presenter.$view.find('#line_tmp').length &gt; 0) {
                    presenter.$view.find('#line_tmp').remove();
                }
            });
            presenter.$view.find('.point_container').click(function(event) {
                event.stopPropagation();
                event.preventDefault();
                if (!presenter.isErrorMode &amp;&amp; !presenter.disabled &amp;&amp; !presenter.isShowAnswersActive &amp;&amp; timeClick) {
                    i = parseInt($(this).attr('order_value'), 10) - 1;
                    presenter.doClick(i);
                }
            });
        }
        presenter.buildKeyboardController();
    };

    presenter.onEventReceived = function (eventName, data) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }
        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
        if (eventName == "GradualShowAnswers") {
            if (!presenter.isGradualShowAnswersActive) {
                presenter.isGradualShowAnswersActive = true;
            }
            if (data.moduleID === presenter.addonID) {
                presenter.gradualShowAnswers(parseInt(data.item, 10));
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.gradualHideAnswers();
        }
    }

    function getMousePositionOnCanvas(e) {
        var rect = presenter.canvas.getBoundingClientRect(), client = {
            x : e.clientX,
            y : e.clientY
        };

        return {
            x : parseInt(client.x - rect.left, 10),
            y : parseInt(client.y - rect.top, 10)
        };
    }

    function scalePoint({x, y}) {
        var scaledPoint = {x: x, y: y};
        if (!presenter.playerController)
            return scaledPoint;

        var scale = presenter.playerController.getScaleInformation();
        if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
            scaledPoint.x = Math.floor(scaledPoint.x / scale.scaleX);
            scaledPoint.y = Math.floor(scaledPoint.y / scale.scaleY);
        }
        return scaledPoint;
    }

    presenter.createPreview = function(view, model) {
        presenter.initiate(view, model);
        var canvasElement = $('&lt;canvas&gt;&lt;/canvas&gt;'),
            ctx = canvasElement[0].getContext('2d');

        canvasElement.attr('width', presenter.$view.find('.pointslines').parent().width());
        canvasElement.attr('height', presenter.$view.find('.pointslines').parent().height());

        presenter.canvasWidth = presenter.$view.find('.pointslines').parent().width;
        presenter.canvasHeight = presenter.$view.find('.pointslines').parent().height;
        presenter.canvas = canvasElement[0];

        presenter.$view.find('.pointslines').append(canvasElement);
        presenter.canvasOffset = canvasElement.offset();

        var coordinatesContainer = $('&lt;div&gt;&lt;/div&gt;'),
            xContainer = $('&lt;div&gt;x: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
            yContainer = $('&lt;div&gt;y: &lt;span class="value"&gt;&lt;/span&gt;&lt;/div&gt;'),
            coloringWrapper = presenter.$view.find('.pointslines');

        coordinatesContainer.css({
            'width' : 35,
            'height' : 22,
            'border' : '1px solid #696969',
            'borderRadius' : '3px',
            'position' : 'absolute',
            'top' : 3,
            'left' : 3,
            'fontSize' : '9px',
            'padding' : '5px',
            'lineHeight' : '11px'
        });

        coordinatesContainer.append(xContainer).append(yContainer);

        coloringWrapper.append(coordinatesContainer);
        coloringWrapper.css({
            'position' : 'relative',
            'minHeight' : presenter.canvasHeight,
            'minWidth' : presenter.canvasWidth
        });

        function setCalculatedPosition(e) {
            xContainer.find('.value').html(getMousePositionOnCanvas(e).x);
            yContainer.find('.value').html(getMousePositionOnCanvas(e).y);
        }

        var doesElementExist = function() {
            var $moduleSelector = $('.moduleSelector[data-id="'+presenter.addonID+'"]');

            if ($moduleSelector.length &gt; 0) {
                $moduleSelector.on('mousemove', function(e) {
                    setCalculatedPosition(e);
                });

                clearInterval(interval);
            }
        };

        var interval = setInterval(function() { doesElementExist(); }, 500);

        canvasElement.on('mousemove', function(e) {
            setCalculatedPosition(e);
        });
    };

    presenter.reset = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        var numberOfPoints = presenter.points.length;
        if (presenter.singleMode) {
            for ( var i = 0; i &lt; numberOfPoints; i++) {
                presenter.pointsConnected[i] = 0;
            }
        };
        for (i = 0; i &lt; numberOfPoints; i++) {
            for (var j = i; j &lt; numberOfPoints; j++) {
                presenter.currentLines[i][j] = presenter.startingLines[i][j];
                if (presenter.startingLines[i][j] == 1 || presenter.startingLines[i][j] == 2) {
                    if (presenter.$view.find('#line_' + (i) + '_' + (j)).length &lt;= 0)
                        presenter.drawLine(i, j);
                    if (presenter.singleMode) {
                        presenter.pointsConnected[i] = 1;
                        presenter.pointsConnected[j] = 1;
                    }
                } else if (presenter.startingLines[i][j] === 0 &amp;&amp; presenter.$view.find('#line_' + (i) + '_' + (j)).length == 1) {
                    presenter.$view.find('#line_' + (i) + '_' + (j)).remove();
                }
            }
        }
        presenter.$view.find('.selected').removeClass('selected');
        presenter.selectedPoint = -1;
        presenter.disabled = presenter.initDisabled;
        if (presenter.disabled) {
            presenter.disable();
        } else {
            presenter.enable();
        }
        presenter.isVisible = presenter.initIsVisible;
        presenter.updateVisibility();
        presenter.setWorkMode();
    };

    presenter.getState = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        return JSON.stringify({
            currentLines : presenter.currentLines,
            disabled : presenter.disabled,
            visible : presenter.isVisible,
            single : presenter.pointsConnected
        });
    };

    presenter.setState = function(state) {
        presenter.currentLines = JSON.parse(state).currentLines;
        presenter.disabled = JSON.parse(state).disabled;
        presenter.pointsConnected = JSON.parse(state).single;
        if (presenter.error == false) {
            if (presenter.disabled) {
                presenter.disable();
            } else {
                presenter.enable();
            }
            presenter.isVisible = JSON.parse(state).visible;
            presenter.updateVisibility();
            var numberOfPoints = presenter.points.length;
            for (var i = 0; i &lt; numberOfPoints; i++) {
                for (var j = i; j &lt; numberOfPoints; j++) {
                    if ((presenter.currentLines[i][j] == 1 || presenter.currentLines[i][j] == 2) &amp;&amp; (presenter.$view.find('#line_' + (i) + '_' + (j)).length &lt;= 0)) {
                        presenter.drawLine(i, j);
                    } else {
                        if (presenter.currentLines[i][j] === 0 &amp;&amp; presenter.$view.find('#line_' + (i) + '_' + (j)).length == 1) {
                            presenter.$view.find('#line_' + (i) + '_' + (j)).remove();
                        }
                    }
                }
            }
        }
    };

    presenter.getMaxScore = function() {
        const isShowAnswersActive = presenter.isShowAnswersActive;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var score = 0;
        if (!presenter.activity || (presenter.error !== false)) {
            return score;
        }

        var numberOfPoints = presenter.points.length;
        for (var i = 0; i &lt; numberOfPoints; i++) {
            for (var j = i; j &lt; numberOfPoints; j++) {
                if ((presenter.startingLines[i][j] === 0 &amp;&amp; presenter.answer[i][j] == 1)
                    || (presenter.startingLines[i][j] == 1 &amp;&amp; presenter.answer[i][j] === 0)
                ) {
                    score++;
                }
            }
        }

        if (isShowAnswersActive) {
            presenter.showAnswers();
        }

        return score;
    };

    presenter.getScore = function(view, model) {
        const isShowAnswersActive = presenter.isShowAnswersActive;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var score = 0;
        if (!presenter.activity || (presenter.error !== false)) {
            return score;
        }

        var numberOfPoints = presenter.points.length;
        for (var i = 0; i &lt; numberOfPoints; i++) {
            for (var j = i; j &lt; numberOfPoints; j++) {
                if (isCorrctConnected(i, j)) {
                    score++;
                }
            }
        }


        if (isShowAnswersActive) {
            presenter.showAnswers();
        }

        return score;
    };

    presenter.getErrorCount = function() {
        const isShowAnswersActive = presenter.isShowAnswersActive;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var errors = 0;
        if (!presenter.activity || (presenter.error !== false)) {
            return errors;
        }

        var numberOfPoints = presenter.points.length;
        for (var i = 0; i &lt; numberOfPoints; i++) {
            for (var j = i; j &lt; numberOfPoints; j++) {
                if (isWrongConnected(i, j)) {
                    errors++;
                }
            }
        }

        if (isShowAnswersActive) {
            presenter.showAnswers();
        }

        return errors;
    };

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.isErrorMode = true;
        if (!presenter.activity || (presenter.error !== false)) {
            return;
        }

        var numberOfPoints = presenter.points.length;
        for (var i = 0; i &lt; numberOfPoints; i++) {
            for (var j = i; j &lt; numberOfPoints; j++) {
                var lineId = '#line_' + i + '_' + j;
                if (isCorrctConnected(i, j)) {
                    presenter.$view.find(lineId).addClass('correctLine');
                } else if (isWrongConnected(i, j)) {
                    presenter.$view.find(lineId).addClass('wrongLine');
                }
            }
        }

        if (presenter.getScore() == presenter.getMaxScore() &amp;&amp; presenter.getErrorCount() === 0 &amp;&amp; !(presenter.isEmpty())) {
            presenter.$view.find('.pointslines').addClass('correct');
        } else if (presenter.getScore() === 0 &amp;&amp; presenter.getErrorCount() === 0) {
        } else {
            presenter.$view.find('.pointslines').addClass('wrong');
        }
    };

    function isCorrctConnected(point1Index, point2Index) {
        if (point1Index &gt; point2Index) {
            let temp = point1Index;
            point1Index = point2Index;
            point2Index = temp;
        }
        return ((presenter.startingLines[point1Index][point2Index] === 0
                &amp;&amp; presenter.answer[point1Index][point2Index] == 1
                &amp;&amp; presenter.currentLines[point1Index][point2Index] == 1)
            || (presenter.startingLines[point1Index][point2Index] == 1
                &amp;&amp; presenter.answer[point1Index][point2Index] === 0
                &amp;&amp; presenter.currentLines[point1Index][point2Index] === 0)
        )
    }

    function isWrongConnected(point1Index, point2Index) {
        if (point1Index &gt; point2Index) {
            let temp = point1Index;
            point1Index = point2Index;
            point2Index = temp;
        }
        return ((presenter.startingLines[point1Index][point2Index] === 0
                &amp;&amp; presenter.answer[point1Index][point2Index] === 0
                &amp;&amp; presenter.currentLines[point1Index][point2Index] == 1)
            || (presenter.startingLines[point1Index][point2Index] == 1
                &amp;&amp; presenter.answer[point1Index][point2Index] == 1
                &amp;&amp; presenter.currentLines[point1Index][point2Index] === 0)
        )
    }

    presenter.setWorkMode = function() {
        presenter.isErrorMode = false;
        presenter.$view.find('.wrongLine').removeClass('wrongLine');
        presenter.$view.find('.correctLine').removeClass('correctLine');
        presenter.$view.find('.pointslines').removeClass('correct');
        presenter.$view.find('.pointslines').removeClass('wrong');
    };

    presenter.gradualShowAnswers = function () {
        if (presenter.activity) {
            presenter.setWorkMode();
            presenter.$view.find('.line').not('.noremovable').css("visibility", "hidden");
            var showingAnswer = presenter.answersNumber;
            var numberOfPoints = presenter.points.length;
            var i, j;
            if (presenter.showAllAnswersInGradualShowAnswersMode) {
                for (i = 0; i &lt; numberOfPoints; i++) {
                    for (j = i; j &lt; numberOfPoints; j++) {
                        if (presenter.answer[i][j] === 1) {
                            presenter.drawLine(i,j,true);
                        }
                    }
                }
            } else {
                for (i = 0; i &lt; numberOfPoints; i++) {
                    for (j = i; j &lt; numberOfPoints; j++) {
                        if (presenter.answer[i][j] === 1) {
                            if (showingAnswer === 0) {
                                presenter.drawLine(i,j,true);
                                presenter.answersNumber++;
                            }
                            showingAnswer--;
                        }
                    }
                }
            }
        }
    }

    presenter.getActivitiesCount = function () {
        if (!presenter.showAllAnswersInGradualShowAnswersMode) {
            var lineCounter = 0;
            var numberOfPoints = presenter.points.length;
            var i, j;
            for (i = 0; i &lt; numberOfPoints; i++) {
                for (j = i; j &lt; numberOfPoints; j++) {
                    if (presenter.answer[i][j] === 1) {
                        lineCounter++;
                    }
                }
            }
            return lineCounter;
        }
        return 1;
    };

    presenter.gradualHideAnswers = function () {
        if (presenter.activity) {
            presenter.answersNumber = 0;
            presenter.isGradualShowAnswersActive = false;
            presenter.$view.find('.line-show-answer').remove();
            presenter.$view.find('.line').css("visibility", "inherit");
        }
    }

    presenter.showAnswers = function () {
        if (presenter.activity) {
            if (presenter.isShowAnswersActive) {
                presenter.hideAnswers();
            }
            presenter.isShowAnswersActive = true;
            presenter.setWorkMode();
            presenter.$view.find('.line').not('.noremovable').css("visibility", "hidden");
            var numberOfPoints = presenter.points.length;
            var i, j;
            for (i = 0; i &lt; numberOfPoints; i++) {
                for (j = i; j &lt; numberOfPoints; j++) {
                    if (presenter.answer[i][j] == 1) {
                        presenter.drawLine(i,j,true);
                    }
                }
            }
        }
    };

    presenter.hideAnswers = function () {
        if (presenter.activity &amp;&amp; presenter.isShowAnswersActive) {
            presenter.isShowAnswersActive = false;
            presenter.$view.find('.line-show-answer').remove();
            presenter.$view.find('.line').css("visibility", "inherit");
        }
    };

    presenter.isShowingAnswers = function () {
        return presenter.isGradualShowAnswersActive || presenter.isShowAnswersActive;
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new PointsLinesKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find(`.point`);
    };

    function PointsLinesKeyboardController(elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    presenter.isTTS = function () {
        return presenter.isWCAGOn &amp;&amp; presenter.getTextToSpeechOrNull(presenter.playerController);
    };

    presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    PointsLinesKeyboardController.prototype = Object.create(KeyboardController.prototype);
    PointsLinesKeyboardController.prototype.constructor = PointsLinesKeyboardController;

    PointsLinesKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    PointsLinesKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    PointsLinesKeyboardController.prototype.switchElement = function (move) {
        let newPositionIndex = this.keyboardNavigationCurrentElementIndex + move;
        if (newPositionIndex &gt;= this.keyboardNavigationElementsLen) {
            newPositionIndex = this.keyboardNavigationCurrentElementIndex;
        } else if (newPositionIndex &lt; 0) {
            newPositionIndex = 0;
        }
        this.markCurrentElement(newPositionIndex);
        this.readCurrentElement();
    };

    PointsLinesKeyboardController.prototype.select = function (event) {
        if (event) {
            event.preventDefault();
        }
        if (!this.isSelectEnabled) {
            return;
        }

        const wasSelected = this.getCurrentElement().hasClass('selected');

        this.selectAction();

        const isSelected = this.getCurrentElement().hasClass('selected');
        if (!wasSelected &amp;&amp; isSelected) {
            this.readSelected();
        } else if (wasSelected &amp;&amp; !isSelected) {
            this.readDeselected();
        }
    };

    PointsLinesKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    PointsLinesKeyboardController.prototype.readCurrentElement = function () {
        if (!presenter.isTTS()) {
            return;
        }

        let textVoices = [];
        const $currentPoint = this.getCurrentElement();
        const currentPointIndex = parseInt($currentPoint.parent().attr('order_value'), 10) - 1;
        textVoices.push(this.createTextVoiceForPoint(currentPointIndex));

        if ($currentPoint.hasClass('selected') &amp;&amp; !presenter.isShowingAnswers()) {
            textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selected));
        }

        const connectedPointsIndexes = getConnectedPointsIndexes(currentPointIndex);
        if (connectedPointsIndexes.length !== 0) {
            textVoices.push(this.createTextVoiceWithLanguageFromLesson(presenter.speechTexts.connectedTo));
            connectedPointsIndexes.forEach(pointIndex =&gt; {
                textVoices.push(this.createTextVoiceForPoint(pointIndex));
                if (!presenter.isErrorMode) {
                    return;
                }

                if (isCorrctConnected(currentPointIndex, pointIndex)) {
                    textVoices.push(this.createTextVoiceWithLanguageFromLesson(presenter.speechTexts.correct));
                } else if (isWrongConnected(currentPointIndex, pointIndex)) {
                    textVoices.push(this.createTextVoiceWithLanguageFromLesson(presenter.speechTexts.wrong));
                }
            })
        }
        presenter.speak(textVoices);
    };
    
    function getConnectedPointsIndexes (rootPointIndex) {
        let connectedPointsIndexes = [];
        if (presenter.isShowingAnswers()) {
            const linesForShowAnswers = presenter.view.querySelectorAll(`.line-show-answer`);
            const attribute1Name = `point1`;
            const attribute2Name = `point2`;
            linesForShowAnswers.forEach(line =&gt; {
                let attribute1Value = parseInt(line.getAttribute(attribute1Name), 10);
                if (attribute1Value === rootPointIndex) {
                    let attribute2Value = parseInt(line.getAttribute(attribute2Name), 10);
                    connectedPointsIndexes.push(attribute2Value);
                } else {
                    let attribute2Value = parseInt(line.getAttribute(attribute2Name), 10);
                    if (attribute2Value === rootPointIndex) {
                        connectedPointsIndexes.push(attribute1Value);
                    }
                }
            })
            connectedPointsIndexes = [...new Set(connectedPointsIndexes)];
        } else {
            let connections = presenter.currentLines[rootPointIndex];
            for (let i = 0; i &lt; rootPointIndex; i++) {
                connections[i] = presenter.currentLines[i][rootPointIndex];
            }
            connections.forEach((value, index) =&gt; {
                if (!value) return;
                connectedPointsIndexes.push(index);
            })
        }
        connectedPointsIndexes.sort();
        return connectedPointsIndexes;
    }

    PointsLinesKeyboardController.prototype.readSelected = function () {
        if (!presenter.isTTS()) {
            return;
        }

        let textVoices = [];
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.selected));
        presenter.speak(textVoices);
    };

    PointsLinesKeyboardController.prototype.readDeselected = function () {
        if (!presenter.isTTS()) {
            return;
        }

        let textVoices = [];
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.deselected));
        presenter.speak(textVoices);
    };

    PointsLinesKeyboardController.prototype.readConnected = function () {
        if (!presenter.isTTS()) {
            return;
        }

        let textVoices = [];
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.connected));
        presenter.speak(textVoices);
    };

    PointsLinesKeyboardController.prototype.readDisconnected = function () {
        if (!presenter.isTTS()) {
            return;
        }

        let textVoices = [];
        textVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.disconnected));
        presenter.speak(textVoices);
    };

    PointsLinesKeyboardController.prototype.createTextVoiceForPoint = function (pointIndex) {
        let message;
        if (presenter.altTexts &amp;&amp; presenter.altTexts[pointIndex]) {
            message = presenter.altTexts[pointIndex];
        } else {
            const isIndexExist = presenter.indexes &amp;&amp; presenter.indexes[pointIndex];
            const index = isIndexExist ? presenter.indexes[pointIndex] : pointIndex + 1;
            const commaToSeparateIndex = ','
            message = `${presenter.speechTexts.point}${commaToSeparateIndex} ${index}`;
        }
        return this.createTextVoiceWithLanguageFromPresenter(message);
    };

    PointsLinesKeyboardController.prototype.createTextVoiceWithLanguageFromLesson = function (message) {
        return window.TTSUtils.getTextVoiceObject(message);
    };

    PointsLinesKeyboardController.prototype.createTextVoiceWithLanguageFromPresenter = function (message) {
        return window.TTSUtils.getTextVoiceObject(message, presenter.langTag);
    };

    presenter.speak = function (data) {
        let tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Points_To_Plot" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="false" name="Source" nameLabel="Points_To_Plot_property_source" type="string"/>
		<property name="Points to plot" nameLabel="Points_To_Plot_property_points_to_plot" type="list">
			<property isLocalized="false" name="plot id" nameLabel="Points_To_Plot_property_plot_id" type="string"/>
			<property isLocalized="false" name="points" nameLabel="Points_To_Plot_property_points" type="int"/>
			<property isLocalized="false" name="strict points" nameLabel="Points_To_Plot_property_strict_points" type="string"/>
		</property>
		<property isLocalized="false" name="Decimal separator" nameLabel="Points_To_Plot_property_decimal_separator" type="string"/>
		<property isLocalized="false" name="Show answers" nameLabel="Points_To_Plot_property_show_answers" type="string"/>
	</model>
<css/><view/><preview/><presenter>function AddonPoints_To_Plot_create() {

    var presenter = function() {
    };
    var eventBus; // Modules communication
    var addonID;
    presenter.data = {
        pointsOnPlot: [],
        selectedPoints: []
    };
    var isShowAnswersActive = false;
    presenter.STATE_CORRECT = 1;
    presenter.STATE_INCORRECT = 0;
    presenter.VERSION = '1.0.2';
    presenter.run = function(view, model) {
        presenter.view = view;
        presenter.model = presenter.upgradeModel(model);

        eventBus = presenter.playerController.getEventBus();
        addonID = model.ID;

        presenter._allDoneState = false;
        presenter.initialize(presenter.model);
        presenter.setEventListeners();
    };

    presenter.upgradeModel = function(model) {
        return presenter.upgradeShowAnswers(model);
    }

    presenter.upgradeShowAnswers = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['Show answers'] === undefined) {
            upgradedModel['Show answers'] = '';
        }

        return upgradedModel;
    };

    presenter.initialize = function(model) {
        this.source = model['Source'];
        this.decimalSeparator = (model['Decimal separator'] === undefined || model['Decimal separator'] == '') ? '.' : model['Decimal separator'];
        if(presenter.decimalSeparator != '.' &amp;&amp; presenter.decimalSeparator != ',') {
            presenter.decimalSeparator = '.';
        };
        $.each(model['Points to plot'], function(idx, val) {
            if(val !== undefined) {
                el = {
                    plotId: val['plot id'],
                    pointsToSelect: parseInt(val['points']),
                    strictPoints: val['strict points'] !== '' ? presenter.parseStrictPoints(val['strict points']) : [],
                    collectedPoints: []
                };
                presenter.data.pointsOnPlot.push(el);
            }
        });
        this.data.selectedPoints = [];
        var rawShowAnswers = model['Show answers'].trim();
        presenter.showAnswersPoints = rawShowAnswers.length &gt; 0 ? presenter.parseStrictPoints(rawShowAnswers) : [];
    };

    presenter.setEventListeners = function() {
        if (eventBus == null) return;
        const events = ["ShowAnswers", "HideAnswers", "GradualShowAnswers", "GradualHideAnswers"];
        events.forEach((eventName) =&gt; {
            eventBus.addEventListener(eventName, this);
        });
    }

    presenter.onEventReceived = function (eventName, eventData) {
         switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                if (eventData.moduleID === addonID) {
                    presenter.gradualShowAnswers(parseInt(eventData.item, 10));
                }
                break;
            case "GradualHideAnswers":
                presenter.hideAnswers();
                break;
        }
    };

    presenter.parseStrictPoints = function(str) {
        var pairs;
        if(this.decimalSeparator == ',') {
            pairs = str.match(/((-?[0-9]+(,?[0-9]+)?){1};{1}(-?[0-9]+(,?[0-9]+)?){1})/g);
        } else {
            pairs = str.match(/((-?[0-9]+(\.?[0-9]+)?){1},{1}(-?[0-9]+(\.?[0-9]+)?){1})/g);
        }
        var points = [];
        $.each(pairs, function(idx, val) {
            var tmp = val.split(presenter.getSeparatorByDecimalSeparator());
            points.push({x: presenter.toDotSeparator(tmp[0]), y: presenter.toDotSeparator(tmp[1])});
        });

        return points;
    };

    presenter.setShowErrorsMode = function() {
        if (isShowAnswersActive) presenter.hideAnswers();
        var sourceModule = this.getSourceModule();
        sourceModule.enableUI(false);
        //check every point
        $.each(this.data.selectedPoints, function(idx, val){
            //if point belongs to any plot mark as correct
            if(val.belongsTo.length &gt; 0) {
                sourceModule.markPointAsCorrect(val.x, val.y);
            } else {
                sourceModule.markPointAsError(val.x, val.y);
            }
        });
    };

    presenter.setWorkMode = function() {
        var sourceModule = this.getSourceModule();
        sourceModule.removePointsStateMarks();
        sourceModule.enableUI(true);
    };

    presenter.showAnswers = function() {
        isShowAnswersActive = true;
        var sourceModule = this.getSourceModule();
        if (sourceModule == null) return;

        sourceModule.setPtpShowAnswersPoints(presenter.showAnswersPoints);
        if (presenter.showAnswersPoints.length &gt; 0) {
            sourceModule.ptpShowAnswers(presenter.showAnswersPoints.length - 1);
        }
    }

    presenter.hideAnswers = function() {
        if (!isShowAnswersActive) return;
        isShowAnswersActive = false;
        var sourceModule = this.getSourceModule();
        if (sourceModule == null) return;
        sourceModule.ptpHideAnswers();
    }

    presenter.getActivitiesCount = function() {
        return presenter.showAnswersPoints.length;
    }

    presenter.gradualShowAnswers = function(item) {
        isShowAnswersActive = true;
        var sourceModule = this.getSourceModule();
        if (sourceModule == null) return;

        sourceModule.setPtpShowAnswersPoints(presenter.showAnswersPoints);
        sourceModule.ptpShowAnswers(item);
    }

    presenter.reset = function() {
        this._allDoneState = false;
        this.data.selectedPoints = [];
        $.each(this.data.pointsOnPlot, function(idx, val) {
            val.collectedPoints = [];
        });
    };
    presenter.getErrorCount = function() {
        var errors = 0;
        $.each(this.data.selectedPoints, function(idx, val){
            //point belongs to any plot
            if(val.belongsTo.length === 0) {
                errors++;
            }
        });
        return errors;
    };

    presenter.getMaxScore = function() {
        var todo = 0;
        $.each(this.data.pointsOnPlot, function(idx, val) {
            todo += val.pointsToSelect;
        });
        return todo;
    };

    presenter.getScore = function() {
        var done = 0;
        $.each(this.data.pointsOnPlot, function(idx, val) {
            done += val.collectedPoints.length &lt;= val.pointsToSelect ? val.collectedPoints.length : val.pointsToSelect;
        });
        return done;
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.getState = function() {
        var state = JSON.stringify({
            version: 1,
            points: presenter.data.selectedPoints,
            plots: presenter.data.pointsOnPlot
            });
        return state;
    };

    presenter.setState = function(state) {
        if(state !== '' &amp;&amp; state !== undefined) {
            state = JSON.parse(state);
            presenter.data.selectedPoints = state.points;
            presenter.data.pointsOnPlot = state.plots;
        }
    };

    presenter.executeCommand = function(name, params) {
    };

    presenter.createPreview = function(view, model) {
        presenter.view = view;
        presenter.model = presenter.upgradeModel(model);
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.onEvent = function(evt, data) {
        switch(evt) {
            case "ValueChanged":
                if(data.item.match(/point_/)) {
                    this.processPointEvent(data);
                }
                break;
        }
    };

    presenter.processPointEvent = function(data) {
        var els = data.item.split('_');
        var x = this.toDotSeparator(els[1]);
        var y = this.toDotSeparator(els[2]);
        var state = data.value == 1 ? true : false;
        if(state) {
            presenter.selectPoint(x,y);
        } else {
            presenter.deselectPoint(x,y);
        }
    };

    presenter.selectPoint = function(x, y) {
        var hasPoint = false;
        $.each(this.data.selectedPoints, function(k, v) {
            if (v.x == x &amp;&amp; v.y == y) {
                hasPoint = true;
                return false;
            }
        });
        if (!hasPoint) {
            var el = {
                x: x,
                y: y,
                belongsTo: presenter.getPointOnPlots(x, y)
            };
            this.data.selectedPoints.push(el);
            this.updatePlotCollectedPoints();
            this.updatePlotsState();
            presenter.stateChanged({
                item: 'point_' + x + '_' + y,
                value: 1,
                score: el.belongsTo.length &gt; 0 ? presenter.STATE_CORRECT : presenter.STATE_INCORRECT
            });
        }
    };

    presenter.deselectPoint = function(x, y) {
        $.each(this.data.selectedPoints, function(k, v) {
            if (v.x == x &amp;&amp; v.y == y) {
                presenter.data.selectedPoints.splice(k, 1);
                presenter.updatePlotCollectedPoints();
                presenter.updatePlotsState();
                return false;
            }
        });
        presenter.stateChanged({
            item: 'point_' + x + '_' + y,
            value: 0,
            score: presenter.STATE_INCORRECT
        });
    };
    /**
     * Checks if point belongs to any plot
     *
     * @param float x - point's x
     * @param float y - point's y
     * @returns Array - list of plots where point belongs to
     */
    presenter.getPointOnPlots = function(x, y) {
        var sourceModule = this.getSourceModule();
        var belongsTo = [];
        $.each(this.data.pointsOnPlot, function(idx, val) {
            // any point
            if (val.strictPoints.length === 0) {
                // check if point belongs to plot
                var res = sourceModule.getPlot().isPointOnPlot(val.plotId, x, y);
                if (res) {
                    belongsTo.push(val.plotId);
                }
            } else {
                // check if point is one of strict points
                $.each(val.strictPoints, function(spIdx, spVal) {
                    if (spVal.x == x &amp;&amp; spVal.y == y) {
                        belongsTo.push(val.plotId);
                        return false;
                    }
                });
            }
        });

        return belongsTo;
    };
    /**
     * Update plot's list of correct points
     *
     * @returns void
     */
    presenter.updatePlotCollectedPoints = function() {
        //remove all collected points for each plot
        $.each(this.data.pointsOnPlot, function(idx, val) {
            val.collectedPoints = [];
        });
        //set new collected points
        $.each(this.data.selectedPoints, function(pidx, pval) {
            $.each(pval.belongsTo, function(btIdx, btVal) {
                presenter.addCollectedPoint(btVal, pval.x, pval.y);
            });
        });
    };
    /**
     * Adds point to plot's collected points
     *
     * @param string plotId - id of plot
     * @param float x - point's x
     * @param float y - point's y
     * @returns void
     */
    presenter.addCollectedPoint = function(plotId, x, y) {
        $.each(this.data.pointsOnPlot, function(idx, val) {
           if(val.plotId == plotId) {
               val.collectedPoints.push({x: x, y: y});
               return false;
           }
        });
    };
    presenter.updatePlotsState = function() {
        $.each(this.data.pointsOnPlot, function(idx, val) {
            res = val.collectedPoints.length &lt; val.pointsToSelect ? false : true;
            presenter.stateChanged({
                item: 'plot_' + val.plotId,
                value: '',
                score: res ? presenter.STATE_CORRECT : presenter.STATE_INCORRECT
            });
        });
    };
    presenter.getSourceModule = function() {
        return this.playerController.getModule(this.source);
    };
    presenter.stateChanged = function(data) {
        if(data) {
            //chained events when data is array
            if(!(data instanceof Array)) {
                //become chain with single event
                data = [data];
            }

            //call chained events
            for(var t=0;t&lt;data.length;t++) {
                data[t].source = addonID;
                data[t].item = data[t].item.toString();
                if(data[t].item.substring(0,6) == 'point_') {
                    data[t].item = presenter.convertValueToDisplay(data[t].item);
                }
                data[t].value = data[t].value.toString();
                data[t].score = data[t].score === null ? null : data[t].score.toString();
                eventBus.sendEvent('ValueChanged', data[t]);
            }
        }

        if (presenter.isAllOK()) {
            if (!presenter._allDoneState) {
                presenter._allDoneState = true;
                presenter.sendAllOKEvent();
            }
        } else {
            presenter._allDoneState = false;
        }
    };
    presenter.sendAllOKEvent = function() {
        var eventData = {
            'source': addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };
    presenter.toDotSeparator = function(value) {
        return (value + '').replace(this.decimalSeparator, '.');
    };
    presenter.getSeparatorByDecimalSeparator = function() {
        return this.decimalSeparator == '.' ? ',' : ';';
    };
    presenter.convertValueToDisplay = function(value) {
        return (value + '').replace(new RegExp('\\.', 'g'), presenter.decimalSeparator);
    };
    presenter.getDecimalSeparator = function() {
        return presenter.decimalSeparator;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Print_Report" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Text" isLocalized="true" name="Text" nameLabel="Print_Report_property_text" type="string"/>
        <property displayName="Print Report Label" isLocalized="true" name="PrintReportLabel" nameLabel="Print_Report_property_print_report_label" type="string"/>
        <property displayName="Close Report Label" isLocalized="true" name="CloseReportLabel" nameLabel="Print_Report_property_close_report_label" type="string"/>
        <property displayName="Show Export Button" name="ShowExportButton" nameLabel="Print_Report_property_show_export_button" type="boolean"/>
        <property displayName="Export Button Label" isLocalized="true" name="ExportCsvLabel" nameLabel="Print_Report_property_export_csv_label" type="string"/>
        <property displayName="Title" name="Title" nameLabel="Print_Report_property_title" type="boolean"/>
        <property displayName="Title Label" isLocalized="true" name="TitleLabel" nameLabel="Print_Report_property_title_label" type="string"/>
        <property displayName="Show subtitle" name="Subtitle" nameLabel="Print_Report_property_subtitle" type="boolean"/>
        <property displayName="Subtitle" isLocalized="true" name="SubtitleLabel" nameLabel="Print_Report_property_subtitle_label" type="string"/>
        <property displayName="Show User First And Last Name" name="Username" nameLabel="Print_Report_property_username" type="boolean"/>
        <property displayName="Percentage Score" name="PercentageScore" nameLabel="Print_Report_property_percentage_score" type="boolean"/>
        <property displayName="Percentage Score Label" isLocalized="true" name="PercentageScoreLabel" nameLabel="Print_Report_property_percentage_score_label" type="string"/>
        <property displayName="Checks" name="Checks" nameLabel="Print_Report_property_checks" type="boolean"/>
        <property displayName="Checks Label" isLocalized="true" name="ChecksLabel" nameLabel="Print_Report_property_checks_label" type="string"/>
        <property displayName="Mistakes" name="Mistakes" nameLabel="Print_Report_property_mistakes" type="boolean"/>
        <property displayName="Mistakes Label" isLocalized="true" name="MistakesLabel" nameLabel="Print_Report_property_mistakes_label" type="string"/>
        <property displayName="Errors" name="Errors" nameLabel="Print_Report_property_errors" type="boolean"/>
        <property displayName="Errors Label" isLocalized="true" name="ErrorsLabel" nameLabel="Print_Report_property_errors_label" type="string"/>
        <property displayName="Page Score" name="PageScore" nameLabel="Print_Report_property_page_score" type="boolean"/>
        <property displayName="Page Score Label" isLocalized="true" name="PageScoreLabel" nameLabel="Print_Report_property_page_score_label" type="string"/>
        <property displayName="Time Per Page" name="TimePerPage" nameLabel="Print_Report_property_time_per_page" type="boolean"/>
        <property displayName="Time Per Page Label" isLocalized="true" name="TimePerPageLabel" nameLabel="Print_Report_property_time_per_page_label" type="string"/>
        <property displayName="Show Total Results" name="Total" nameLabel="Print_Report_property_total" type="boolean"/>
        <property displayName="Total Results Label" isLocalized="true" name="TotalLabel" nameLabel="Print_Report_property_total_label" type="string"/>
        <property displayName="Show Current Date" name="Date" nameLabel="Print_Report_property_date" type="boolean"/>
        <property displayName="Date Format" isLocalized="true" name="DateLabel" nameLabel="Print_Report_property_date_label" type="{dd-mm-yy, mm-dd-yy, yy-mm-dd, dd/mm/yy, mm/dd/yy, yy/mm/dd}"/>
        <property displayName="Time Per Page: Days Label" isLocalized="true" name="TimePerPageDaysLabel" nameLabel="Print_Report_property_time_per_page_days_label" type="string"/>
        <property displayName="Time Per Page: Hours Label" isLocalized="true" name="TimePerPageHoursLabel" nameLabel="Print_Report_property_time_per_page_hours_label" type="string"/>
        <property displayName="Time Per Page: Minutes Label" isLocalized="true" name="TimePerPageMinutesLabel" nameLabel="Print_Report_property_time_per_page_minutes_label" type="string"/>
        <property displayName="Time Per Page: Seconds Label" isLocalized="true" name="TimePerPageSecondsLabel" nameLabel="Print_Report_property_time_per_page_seconds_label" type="string"/>
        <property displayName="User Form: First Name Label" isLocalized="true" name="UsernameFirstLabel" nameLabel="Print_Report_property_username_first_label" type="string"/>
        <property displayName="User Form: Last Name Label" isLocalized="true" name="UsernameLastLabel" nameLabel="Print_Report_property_username_last_label" type="string"/>
        <property displayName="User Form: Confirm Label" isLocalized="true" name="UsernameConfirmLabel" nameLabel="Print_Report_property_username_confirm_label" type="string"/>
        <property displayName="User Form: Cancel Label" isLocalized="true" name="UsernameCancelLabel" nameLabel="Print_Report_property_username_cancel_label" type="string"/>
        <property displayName="Styles" name="Styles" nameLabel="Print_Report_property_styles" type="text"/>
    </model>
<css>.print-report-addon-wrapper {
    width: 100%;
    height: 100%;
    cursor: pointer;
}

.print-report-popup {
    position: absolute;
    z-index: 9999;
    background: rgba(0,0,0,0.5);
    color: #000000;
}

.print-report-popup .print-report-form {
    display: table;
    padding: 15px;
    margin: 10px auto 0 auto;
    background: #ffffff;
}

.print-report-popup .print-report-form label,
.print-report-popup .print-report-form input {
    display: block;
}

.print-report-popup .print-report-form label {
    margin: 5px 0;
}

.print-report-popup .print-report-form input {
    height: 34px;
    width: 100%;
    min-width: 300px;
    padding: 6px 12px;
    border: 1px solid #cccccc;
    box-sizing: border-box;
    display: block;
    font-size: 14px;
    line-height: 1.42857143;
    border-radius: 6px;
    color: #555;
    background-color: #ffffff;
    background-image: none;
}

.print-report-popup .print-report-form .print-report-form-actions {
    text-align: center;
}

.print-report-popup .print-report-form .print-report-form-actions button {
    display: inline-block;
    padding: 6px 12px;
    margin: 5px 5px 0 5px;
    font-size: 14px;
    font-weight: 400;
    line-height: 1.42857143;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    background-image: none;
    border: 1px solid transparent;
    border-radius: 4px;
    cursor: pointer;
}

.print-report-popup .print-report-form button.print-report-form-confirm-btn {
    background: #0000ff;
    color: #ffffff;
}

.print-report-popup .print-report-form button.export-report-form-confirm-btn {
    background: #0000ff;
    color: #ffffff;
}

.print-report-popup .print-report-form button.print-report-form-cancel-btn {
    background: #ff0000;
    color: #ffffff;
}

.export-button {
    width: 100%;
}
</css><view/><preview/><presenter>function AddonPrint_Report_create(){
    var presenter = function AddonPrint_Report_presenter () {};

    presenter.logic = function addonPrint_Report_logic (view, model, preview) {
        presenter.configuration = presenter.validateModel(model);
        presenter.$body = $('body');
        presenter.$view = $(view);
        presenter.$page = $('.ic_page');
        presenter.$wrapper = $('&lt;div&gt;&lt;/div&gt;').addClass('print-report-addon-wrapper').text(presenter.configuration.text);
        presenter.$exportButton = $('&lt;button&gt;&lt;/button&gt;').addClass('export-button').text(presenter.configuration.labels.exportCsvReport);
        presenter.$view.append(presenter.$exportButton);
        presenter.$view.append(presenter.$wrapper);

        if(!presenter.configuration.report.showExportButton){
            presenter.$exportButton.hide();
        }

        presenter.originalViewStyles = presenter.$view.attr('style');
        presenter.originalViewClasses = presenter.$view.attr('class');

        presenter.user = {
            'firstName': '',
            'lastName': ''
        };

        if (!preview) {
            presenter.$wrapper.click(presenter.clickAction);
            presenter.$popup = presenter.createPopup();
            presenter.$view.append(presenter.$popup);
            presenter.$popup.hide();
            presenter.bindPopupEvents();
            presenter.$exportButton.on("click", function () {
                presenter.showPopup(true);
            });
        }
    };

    presenter.exportButtonClickAction = function () {
        var data = presenter.getPagesData(),
            name = presenter.user.firstName || '',
            lastName = presenter.user.lastName || '',
            date = presenter.prepareDate(presenter.configuration.labels.date),
            firstNameLabel = presenter.configuration.labels.userFirstName,
            lastNameLabel = presenter.configuration.labels.userLastName,
            scoreLabel = presenter.configuration.labels.pageScore || "Score",
            errorsLabel = presenter.configuration.labels.errors || "Errors";

            var blob = new Blob([firstNameLabel+ ": " + name + ", " + lastNameLabel + ": " + lastName + ", " + scoreLabel + ": " + data.total.score + "/" + data.total.maxScore + ", " + errorsLabel + ": " + data.total.errors + ", " + date], {type: "text/plain;charset=utf-8"});
            saveAs(blob, "report.txt");
    };

    presenter.clickAction = function addonPrint_Report_clickAction () {
        if (presenter.configuration.report.username) {
            presenter.showPopup(false);
        } else {
            presenter.showReport();
        }
    };

    presenter.getPagesData = function addonPrint_Report_getPagesData () {
        var data = {}, page, total;

        for(var i = 0; i &lt; presenter.pageCount; i += 1) {
            page = presenter.presentation.getPage(i);
            if (page.isReportable()) {
                data[i + 1] = presenter.getPageData(page);
            }
        }
        page = null;

        total = presenter.calculateTotal(data);

        return {
            pages: data,
            total: total
        };
    };

    presenter.getPageData = function addonPrint_Report_getPageData (page) {
        var id = page.getId(),
            score = presenter.scoreService.getPageScoreById(id),
            time = presenter.timeService.getPageTimeById(id),
            percentageScore;

        if (score.score === 0 &amp;&amp; score.maxScore === 0) {
            if (page.isVisited()) {
                percentageScore = 100;
            } else {
                percentageScore = 0;
            }
        } else {
            percentageScore = parseInt(Math.floor( (score.score / score.maxScore) * 100 ));
        }

        return {
            'name': page.getName(),
            'score': score.score,
            'maxScore': score.maxScore,
            'percentageScore': percentageScore,
            'mistakes': score.mistakeCount,
            'errors': score.errorCount,
            'checks': score.checkCount,
            'time': parseInt(time),
            'visited': page.isVisited()
        };
    };

    presenter.calculateTotal = function addonPrint_Report_calculateTotal (data) {
        var count = 0, total = {
            'score': 0,
            'maxScore': 0,
            'percentageScore': 0,
            'mistakes': 0,
            'errors': 0,
            'checks': 0,
            'time': 0
        };

        $.each(data, function AddonPrint_Report_calculateTotalEach (_, page) {
            count += 1;
            total.score += page.score;
            total.maxScore += page.maxScore;
            total.percentageScore += page.percentageScore;
            total.mistakes += page.mistakes;
            total.errors += page.errors;
            total.checks += page.checks;
            total.time += page.time;
        });

        total.percentageScore = parseInt(Math.floor( total.percentageScore / count ));
        return total;
    };

    presenter.createPopup = function addonPrint_Report_createPopup () {
        var $popup = $('&lt;div&gt;&lt;/div&gt;').
                addClass('print-report-popup'),
            $form = $('&lt;div&gt;&lt;/div&gt;').
                addClass('print-report-form'),
            $firstNameLabel = $('&lt;label&gt;&lt;/label&gt;').
                addClass('print-report-form-firstname-label').
                text(presenter.configuration.labels.userFirstName),
            $firstName = $('&lt;input&gt;').
                attr('type', 'text').
                attr('value', presenter.user.firstName).
                addClass('print-report-form-firstname'),
            $lastNameLabel = $('&lt;label&gt;&lt;/label&gt;').
                addClass('print-report-form-lastname-label').
                text(presenter.configuration.labels.userLastName),
            $lastName = $('&lt;input&gt;').
                attr('type', 'text').
                attr('value', presenter.user.lastName).
                addClass('print-report-form-lastname'),
            $confirmBtn = $('&lt;button&gt;&lt;/button&gt;').
                addClass('print-report-form-confirm-btn').
                text(presenter.configuration.labels.userConfirm),
            $exportBtn = $('&lt;button&gt;&lt;/button&gt;').
                addClass('export-report-form-confirm-btn').
                text(presenter.configuration.labels.exportCsvReport),
            $cancelBtn = $('&lt;button&gt;&lt;/button&gt;').
                addClass('print-report-form-cancel-btn').
                text(presenter.configuration.labels.userCancel);

        $form.append(
                $('&lt;div&gt;&lt;/div&gt;').
                    addClass('print-report-form-firstname-wrapper').
                    append($firstNameLabel).
                    append($firstName)
            ).
            append(
                $('&lt;div&gt;&lt;/div&gt;').
                    addClass('print-report-form-lastname-wrapper').
                    append($lastNameLabel).
                    append($lastName)
            ).
            append(
                $('&lt;div&gt;&lt;/div&gt;').
                    addClass('print-report-form-actions').
                    append($cancelBtn).
                    append($confirmBtn).
                    append($exportBtn)
            );

        $popup.css({
            width: presenter.$page.width() + 'px',
            height: presenter.$page.height() + 'px'
        });

        $popup.append($form);

        return $popup;
    };

    presenter.bindPopupEvents = function addonPrint_Report_bindPopupEvents () {
        presenter.$popup.on('click', '.print-report-form-confirm-btn', function addonPrint_Report_onConfirmClick (event) {
            event.preventDefault();
            presenter.hidePopup();
            presenter.showReport();
        });

        presenter.$popup.on('click', '.export-report-form-confirm-btn', function addonPrint_Report_onConfirmClick (event) {
            event.preventDefault();
            presenter.hidePopup();
            presenter.exportButtonClickAction();
        });

        presenter.$popup.on('click', '.print-report-form-cancel-btn', function addonPrint_Report_onCancelClick (event) {
            event.preventDefault();
            presenter.hidePopup();
        });

        presenter.$popup.on('input', '.print-report-form-firstname', function addonPrint_Report_onFirstNameInput () {
            presenter.user.firstName = $(this).val();
        });

        presenter.$popup.on('input', '.print-report-form-lastname', function addonPrint_Report_onLastNameInput () {
            presenter.user.lastName = $(this).val();
        });
    };

    presenter.unbindPopupEvents = function addonPrint_Report_unbindPopupEvents () {
        presenter.$popup.off();
    };

    presenter.showPopup = function AddonPrint_Report_showPopup (exportButtonClicked) {
        if(exportButtonClicked){
            presenter.$popup.find('.print-report-form-confirm-btn').hide();
        }else{
            presenter.$popup.find('.export-report-form-confirm-btn').hide();
        }

        var $firstName = presenter.$popup.find('.print-report-form-firstname'),
            $lastName = presenter.$popup.find('.print-report-form-lastname');

        $firstName.attr('value', presenter.user.firstName);
        $lastName.attr('value', presenter.user.lastName);

        $firstName = null;
        $lastName = null;

        presenter.$wrapper.hide();
        presenter.$exportButton.hide();

        presenter.$view.
            removeAttr('style class').
            css({
                'width': presenter.$page.width() + 'px',
                'height': presenter.$page.height() + 'px',
                'z-index': 9999
            });

        presenter.$popup.show();
        presenter.$body.animate({scrollTop: presenter.$popup.offset().top}, 'fast', 'swing');
    };

    presenter.hidePopup = function AddonPrint_Report_hidePopup () {
        presenter.$popup.hide();

        presenter.$view.
            removeAttr('style class').
            attr('style', presenter.originalViewStyles).
            attr('class', presenter.originalViewClasses);

        presenter.$popup.find('.print-report-form-confirm-btn').show();
        presenter.$popup.find('.export-report-form-confirm-btn').show();
        presenter.$wrapper.show();
        if(presenter.configuration.report.showExportButton){
            presenter.$exportButton.show();
        }
    };

    presenter.showReport = function addonPrint_Report_showReport () {
        var data = presenter.getPagesData(),
            $reportHtml = presenter.prepareReportHtml(data),
            reportWindow = window.open();

        $(reportWindow.document).ready(function addonPrint_Report_onReportWindowReady () {
            $(reportWindow.document.body).html($reportHtml);
            reportWindow = null;
            $reportHtml = null;
        });
    };

    presenter.prepareReportHtml = function addonPrint_Report_prepareReportHtml (data) {
        var $reportWrapper = $('&lt;section&gt;&lt;/section&gt;').addClass('wrapper'),
            $reportDefaultStyles = $('&lt;style&gt;&lt;/style&gt;'),
            $reportStyles = $('&lt;style&gt;&lt;/style&gt;').text(presenter.configuration.styles),
            $reportActions = presenter.prepareReportActionsHtml(),
            $reportHeader = presenter.prepareReportHeaderHtml(),
            $reportTable = presenter.prepareReportTableHtml(data);

        $reportDefaultStyles.text(
            '@media print and (color) { * { -webkit-print-color-adjust: exact; print-color-adjust: exact; } }' +
            'body { width: 210mm; margin: 0 auto; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;' +
                'font-size: 14px; line-height: 20px; color: #333333; padding: 0; }' +
            'h1, h2 { line-height: 40px; margin: 0; }' +
            'h1 { font-size: 38.5px; }' +
            'h2 { font-size: 31.5px; }' +
            'table { width: 100%; max-width: 100%; border-collapse: collapse; border-spacing: 0; ' +
            'background-color: transparent; }' +
            'table th, table td { padding: 8px; line-height: 20px; text-align: left; vertical-align: top; }' +
            'table td { border-top: 1px solid #dddddd; }' +
            'table tbody &gt; tr:nth-child(odd) td { background: #f9f9f9; }' +
            '.percentage-score-label, .percentage-score { color: blue; }' +
            '.checks-label, .checks { color: green; }' +
            '.mistakes-label, .mistakes { color: brown; }' +
            '.errors-label, .errors { color: red; }' +
            '.page-score-label, .page-score { color: blue; font-weight: bold; }' +
            '.total td { border-top: 3px solid #dddddd; font-weight: bold; }' +
            '.actions { border: 1px solid #ccc; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; ' +
                'border-top-width: 0; margin-bottom: 10px; overflow: auto; background: #fff; }' +
            '.actions button { display: inline-block; padding: 6px 12px; margin: 5px; font-size: 14px;' +
                'font-weight: bold; line-height: 1.42857143; text-align: center; white-space: nowrap;' +
                'vertical-align: middle; background-image: none; border: 1px solid transparent; border-radius: 4px;' +
                'cursor: pointer; }' +
            '.actions button.close { background: #ff0000; color: #ffffff; float: left; }' +
            '.actions button.print { background: #0000ff; color: #ffffff; float: right; }' +
            '@media print { body { width: 100% } .actions { display: none; } }'
        );

        $reportWrapper.append($reportDefaultStyles);
        $reportWrapper.append($reportStyles);
        $reportWrapper.append($reportActions);
        $reportWrapper.append($reportHeader);
        $reportWrapper.append($reportTable);

        return $reportWrapper;
    };

    presenter.prepareReportActionsHtml = function addonPrint_Report_prepareReportActionsHtml () {
        var $actions = $('&lt;div&gt;&lt;/div&gt;').addClass('actions');

        $actions.append(
            $('&lt;button&gt;&lt;/button&gt;').
                addClass('close').
                text(presenter.configuration.labels.closeReport).
                attr('onclick', 'javascript:window.close()')
        ).
        append(
            $('&lt;button&gt;&lt;/button&gt;').
                addClass('print').
                text(presenter.configuration.labels.printReport).
                attr('onclick', 'javascript:window.print()')
        );

        return $actions;
    };

    presenter.prepareMonthFormat = function addonPrint_Report_prepareMonthFormat (month) {
        if(month.toString().length &lt; 2){
            return "0" + month;
        }

        return month;
    };

    presenter.prepareYearFormat = function addonPrint_Report_prepareYearFormat (year) {
        return year.toString().substring(2);
    };

    presenter.prepareDate = function addonPrint_Report_prepareDate (format) {
        var date = new Date(),
            day = date.getDate(),
            month = presenter.prepareMonthFormat(date.getMonth()+1),
            year = presenter.prepareYearFormat(date.getFullYear());


        switch(format) {
            case "dd-mm-yy":
                return day + "-" + month + "-" + year;
                break;
            case "mm-dd-yy":
                return month + "-" + day + "-" + year;
                break;
            case "yy-mm-dd":
                return year + "-" + month + "-" + day;
                break;
            case "dd/mm/yy":
                return day + "/" + month + "/" + year;
                break;
            case "mm/dd/yy":
                return month + "/" + day + "/" + year;
                break;
            case "yy/mm/dd":
                return year + "/" + month + "/" + day;
                break;
            default:
                return day + "-" + month + "-" + year;
        }
    };

    presenter.prepareReportHeaderHtml = function addonPrint_Report_prepareReportHeaderHtml () {
        var $header = $('&lt;div&gt;&lt;/div&gt;').addClass('header');

        if (presenter.configuration.report.title) {
            $header.append(
                $('&lt;div&gt;&lt;/div&gt;').addClass('title').append(
                    $('&lt;h1&gt;&lt;/h1&gt;').text(presenter.configuration.labels.title)
                )
            );
        }

        if (presenter.configuration.report.subtitle) {
            $header.append(
                $('&lt;div&gt;&lt;/div&gt;').addClass('subtitle').append(
                    $('&lt;h2&gt;&lt;/h2&gt;').text(presenter.configuration.labels.subtitle)
                )
            );
        }

        if(presenter.configuration.report.date) {
            $header.append(
                $('&lt;div&gt;&lt;/div&gt;').addClass('date').append(
                    $('&lt;h2&gt;&lt;/h2&gt;').text(presenter.prepareDate(presenter.configuration.labels.date))
                )
            )
        }

        if (presenter.configuration.report.username) {
            $header.append(
                $('&lt;div&gt;&lt;/div&gt;').addClass('user').append(
                        $('&lt;span&gt;&lt;/span&gt;').addClass('firstname').text(presenter.user.firstName || '')
                    ).
                    append(' ').
                    append(
                        $('&lt;span&gt;&lt;/span&gt;').addClass('lastname').text(presenter.user.lastName || '')
                    )
            );
        }

        return $header;
    };

    presenter.prepareReportTableHtml = function addonPrint_Report_prepareReportTableHtml (data) {
        var $report = $('&lt;div&gt;&lt;/div&gt;').addClass('report'),
            $table = $('&lt;table&gt;&lt;/table&gt;'),
            $header = presenter.prepareReportTableHeaderHtml(),
            $body = presenter.prepareReportTableBodyHtml(data.pages, data.total);

        $table.append($header).append($body);
        $report.append($table);

        return $report;
    };

    presenter.prepareReportTableHeaderHtml = function addonPrint_Report_prepareReportTableHeaderHtml () {
        var $header = $('&lt;thead&gt;&lt;/thead&gt;'),
            $row = $('&lt;tr&gt;&lt;/tr&gt;');

        $row.append(
            $('&lt;th&gt;&lt;/th&gt;').addClass('name-label')
        );

        if (presenter.configuration.report.percentageScore) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('percentage-score-label').text(presenter.configuration.labels.percentageScore)
            );
        }

        if (presenter.configuration.report.checks) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('checks-label').text(presenter.configuration.labels.checks)
            );
        }

        if (presenter.configuration.report.mistakes) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('mistakes-label').text(presenter.configuration.labels.mistakes)
            );
        }

        if (presenter.configuration.report.errors) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('errors-label').text(presenter.configuration.labels.errors)
            );
        }

        if (presenter.configuration.report.pageScore) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('page-score-label').text(presenter.configuration.labels.pageScore)
            );
        }

        if (presenter.configuration.report.timePerPage) {
            $row.append(
                $('&lt;th&gt;&lt;/th&gt;').addClass('time-per-page-label').text(presenter.configuration.labels.timePerPage)
            );
        }

        $header.append($row);
        return $header;
    };

    presenter.prepareReportTableBodyHtml = function addonPrint_Report_prepareReportTableBodyHtml (pagesData, total) {
        var $body = $('&lt;tbody&gt;&lt;/tbody&gt;');

        $.each(pagesData, function AddonPrint_Report_prepareReportTableBodyHtmlEach (_, page) {
            $body.append(
                presenter.prepareReportTableBodyRowHtml(page)
            );
        });

        if (presenter.configuration.report.total) {
            total.name = presenter.configuration.labels.total;

            $body.append(
                presenter.prepareReportTableBodyRowHtml(total).addClass('total')
            );
        }

        return $body;
    };

    presenter.prepareReportTableBodyRowHtml = function addonPrint_Report_prepareReportTableBodyRowHtml (pageData) {
        var $row = $('&lt;tr&gt;&lt;/tr&gt;');

        $row.append(
            $('&lt;td&gt;&lt;/td&gt;').addClass('name').text(pageData.name)
        );

        if (presenter.configuration.report.percentageScore) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('percentage-score').text(pageData.percentageScore + '%')
            );
        }

        if (presenter.configuration.report.checks) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('checks').text(pageData.checks)
            );
        }

        if (presenter.configuration.report.mistakes) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('mistakes').text(pageData.mistakes)
            );
        }

        if (presenter.configuration.report.errors) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('errors').text(pageData.errors)
            );
        }

        if (presenter.configuration.report.pageScore) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('page-score').text(pageData.score + '/' + pageData.maxScore)
            );
        }

        if (presenter.configuration.report.timePerPage) {
            $row.append(
                $('&lt;td&gt;&lt;/td&gt;').addClass('time-per-page').text(
                    presenter.humanReadableTime(pageData.time,
                        {
                            days: presenter.configuration.labels.timePerPageDays,
                            hours: presenter.configuration.labels.timePerPageHours,
                            minutes: presenter.configuration.labels.timePerPageMinutes,
                            seconds: presenter.configuration.labels.timePerPageSeconds
                        })
                )
            );
        }

        return $row;
    };

    presenter.humanReadableTime = function addonPrint_Report_humanReadableTime (time, labels) {
        var seconds, minutes,hours, days, output = '';

        seconds = parseInt( Math.floor( time / 1000 ) );

        minutes = parseInt( Math.floor( seconds / 60 ) );

        if (minutes &gt; 0) {
            seconds = seconds - (minutes * 60);
        }

        hours = parseInt( Math.floor( minutes / 60 ) );

        if (hours &gt; 0) {
            minutes = minutes - (hours * 60);
        }

        days = parseInt( Math.floor( hours / 24 ) );

        if (days &gt; 0) {
            hours = hours - (days * 24);
        }

        output = seconds + labels.seconds;

        if (minutes &gt; 0) {
            output = minutes + labels.minutes + ' ' + output;
        }
        if (hours &gt; 0) {
            output = hours + labels.hours + ' ' + output;
        }
        if (days &gt; 0) {
            output = days + labels.days + ' ' + output;
        }

        return output;
    };

    presenter.executeCommand = function addonPrint_Report_executeCommand (name) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, [], presenter);
    };

    presenter.setVisibility =  function addonPrint_Report_setVisibility (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.setPlayerController = function addonPrint_Report_setPlayerController (controller) {
        presenter.playerController = controller;
        presenter.presentation = controller.getPresentation();
        presenter.pageCount = presenter.presentation.getPageCount();
        presenter.scoreService = controller.getScore();
        presenter.timeService = controller.getTimeService();
    };

    presenter.createPreview = function addonPrint_Report_createPreview (view, model) {
        presenter.logic(view, model, true);
    };

    presenter.run = function addonPrint_run (view, model){
        presenter.logic(view, model, false);

        presenter.view = view;

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.destroy = function addonPrint_Report_destroy () {
        presenter.$wrapper.off();
        presenter.$exportButton.off();
        presenter.unbindPopupEvents();

        presenter.user = null;
        presenter.configuration = null;
        presenter.originalViewStyles = null;
        presenter.originalViewClasses = null;

        presenter.playerController = null;
        presenter.presentation = null;
        presenter.pageCount = null;
        presenter.scoreService = null;
        presenter.timeService = null;

        presenter.$popup = null;
        presenter.$wrapper = null;
        presenter.$page = null;
        presenter.$view = null;
        presenter.$body = null;
        presenter.view = null;
        presenter.$exportButton = null;

        presenter.destroy = null;
        presenter = null;
    };

    presenter.show = function addonPrint_Report_show () {
        if (!presenter.configuration.isVisible) {
            presenter.setVisibility(true);
            presenter.configuration.isVisible = true;
        }
    };

    presenter.hide = function addonPrint_Report_hide () {
        if (presenter.configuration.isVisible) {
            presenter.setVisibility(false);
            presenter.configuration.isVisible = false;
        }
    };

    presenter.reset = function addonPrint_Report_reset () {
        presenter.hidePopup();
        presenter.user.firstName = '';
        presenter.user.lastName = '';
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
    };

    presenter.getState = function addonPrint_Report_getState () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            user: presenter.user
        });
    };

    presenter.setState = function addonPrint_Report_setState (state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsedState = JSON.parse(state);

        if (parsedState.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        if (parsedState.user) {
            presenter.user.firstName = parsedState.user.firstName || '';
            presenter.user.lastName = parsedState.user.lastName || '';
        } else {
            presenter.user.firstName = '';
            presenter.user.lastName = '';
        }
    };

    presenter.validateModel = function addonPrint_Report_validateModel (model) {
        return {
            'report': {
                'title': ModelValidationUtils.validateBoolean(model.Title),
                'subtitle': ModelValidationUtils.validateBoolean(model.Subtitle),
                'username': ModelValidationUtils.validateBoolean(model.Username),
                'percentageScore': ModelValidationUtils.validateBoolean(model.PercentageScore),
                'checks': ModelValidationUtils.validateBoolean(model.Checks),
                'mistakes': ModelValidationUtils.validateBoolean(model.Mistakes),
                'errors': ModelValidationUtils.validateBoolean(model.Errors),
                'pageScore': ModelValidationUtils.validateBoolean(model.PageScore),
                'timePerPage': ModelValidationUtils.validateBoolean(model.TimePerPage),
                'total': ModelValidationUtils.validateBoolean(model.Total),
                'date': ModelValidationUtils.validateBoolean(model.Date),
                'showExportButton': ModelValidationUtils.validateBoolean(model.ShowExportButton)
            },
            'labels': {
                'title': model.TitleLabel,
                'subtitle': model.SubtitleLabel,
                'percentageScore': model.PercentageScoreLabel,
                'checks': model.ChecksLabel,
                'mistakes': model.MistakesLabel,
                'errors': model.ErrorsLabel,
                'pageScore': model.PageScoreLabel,
                'timePerPage': model.TimePerPageLabel,
                'total': model.TotalLabel,
                'timePerPageDays': model.TimePerPageDaysLabel || 'd',
                'timePerPageHours': model.TimePerPageHoursLabel || 'h',
                'timePerPageMinutes': model.TimePerPageMinutesLabel || 'm',
                'timePerPageSeconds': model.TimePerPageSecondsLabel || 's',
                'userFirstName': model.UsernameFirstLabel || 'First name',
                'userLastName': model.UsernameLastLabel || 'Last name',
                'userConfirm': model.UsernameConfirmLabel || 'Generate',
                'userCancel': model.UsernameCancelLabel || 'Cancel',
                'closeReport': model.CloseReportLabel || 'Close',
                'printReport': model.PrintReportLabel || 'Print',
                'exportCsvReport': model.ExportCsvLabel || 'Export report',
                'date': model.DateLabel

            },
            'styles': model.Styles,
            'text': model.Text,
            'isVisible': ModelValidationUtils.validateBoolean(model['Is Visible']),
            'isVisibleByDefault': ModelValidationUtils.validateBoolean(model['Is Visible']),
            'ID': model['ID']
        };
    };

    presenter.setShowErrorsMode = function addonPrint_Report_setShowErrorsMode () {};

    presenter.setWorkMode = function addonPrint_Report_setWorkMode () {};

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Programmed_Drawing" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property name="Rows" nameLabel="Programmed_Drawing_property_rows" type="string"/>
        <property name="Columns" nameLabel="Programmed_Drawing_property_columns" type="string"/>
        <property name="Color" nameLabel="Programmed_Drawing_property_color" type="string"/>
        <property name="Initial design" nameLabel="Programmed_Drawing_property_initial_design" type="text"/>
	</model>
<css>.cellGridWrapper {
    width: 100%;
    height: 100%;
}

.cellGrid {
    margin: 0;
    padding: 0;
    border: none;
}

.cell-element {
    float: left;
    padding: 0;
    margin: 0;
    color: black;
    width: 100%;
    height: 100%;
}

.cell-element-wrapper {
    float: left;
    padding: 0;
    margin: 0;
    border-style: solid;
    border-color: black;
    border-width: 1px;
    -webkit-box-sizing:border-box;
    -moz-box-sizing:border-box;
    box-sizing:border-box;
}
</css><view>&lt;div class="cellGridWrapper"&gt;
    &lt;div class="cellGrid"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="cellGridWrapper"&gt;
    &lt;div class="cellGrid"&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonProgrammed_Drawing_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.lastEvent = null;


    var viewContainer;
    var gridContainerWrapper;
    var gridContainer;

    function initGrid(model, preview) {
        var rows = presenter.configuration.rows;
        var columns = presenter.configuration.columns;

        for(var row = 0; row &lt; rows; row++) {
            for(var column = 0; column &lt; columns; column++) {
                var wrapperElement = $(document.createElement('div'));
                wrapperElement.addClass('cell-element-wrapper');

                var selectableElement = $(document.createElement('div'));
                selectableElement.addClass('cell-element');
                selectableElement.attr('coordinates', (column+1)+"-"+((rows-row)));

                wrapperElement.append(selectableElement);
                gridContainer.append(wrapperElement);
            }
        }

        var gridContainerWrapperDimensions = getElementDimensions(gridContainerWrapper);
        var gridContainerWrapperDistances = calculateInnerDistance(gridContainerWrapperDimensions);

        var wrapperDimensions = getElementDimensions(gridContainerWrapper.find('.cell-element-wrapper:first')[0]);
        var wrapperDistances = calculateInnerDistance(wrapperDimensions);

        var elementDimensions = getElementDimensions(gridContainerWrapper.find('.cell-element:first')[0]);
        var elementDistances = calculateInnerDistance(elementDimensions);

        var wrapperWidth = parseInt((model.Width - gridContainerWrapperDistances.horizontal - (wrapperDistances.horizontal * columns)) / columns, 10);
        var wrapperHeight = parseInt((model.Height - gridContainerWrapperDistances.vertical - (wrapperDistances.vertical * rows)) / rows, 10);

        var elementWidth = wrapperWidth - elementDistances.horizontal;
        var elementHeight = wrapperHeight - elementDistances.vertical;

        var newContainerWrapperHeight = wrapperHeight * rows + wrapperDistances.vertical * rows;
        var newContainerWrapperWidth = wrapperWidth * columns + wrapperDistances.horizontal * columns;

        var verticalGapHeight = model.Height - newContainerWrapperHeight;
        var horizontalGapHeight = model.Width - newContainerWrapperWidth;

        gridContainerWrapper.css('height', model.Height + 'px');
        gridContainerWrapper.css('width', model.Width + 'px');
        gridContainer.css('height', model.Height + 'px');
        gridContainer.css('width', (parseInt(model.Width)+parseInt(elementWidth/2)) + 'px');

        var vertical = verticalGapHeight / rows;
        var horizontal = horizontalGapHeight / columns;

        gridContainer.find(".cell-element-wrapper").each(function() {
            var index = $(this).index();
            var selectedRow = parseInt(index / columns, 10);
            var selectedColumn = parseInt(index % columns, 10);

            $(this).width(wrapperWidth + horizontal + 2);
            $(this).height(wrapperHeight + vertical + 2);

            var selectableElement = $(this).find('.cell-element:first');

            var lineHeight = selectedRow === rows -1 ? elementHeight + verticalGapHeight : elementHeight;
            selectableElement.css('line-height', lineHeight + "px");
        });
    }

    function getElementDimensions(element) {
        element = $(element);

        return {
            border:{
                top:parseInt(element.css('border-top-width'), 10),
                bottom:parseInt(element.css('border-bottom-width'), 10),
                left:parseInt(element.css('border-left-width'), 10),
                right:parseInt(element.css('border-right-width'), 10)
            },
            margin:{
                top:parseInt(element.css('margin-top'), 10),
                bottom:parseInt(element.css('margin-bottom'), 10),
                left:parseInt(element.css('margin-left'), 10),
                right:parseInt(element.css('margin-right'), 10)
            },
            padding:{
                top:parseInt(element.css('padding-top'), 10),
                bottom:parseInt(element.css('padding-bottom'), 10),
                left:parseInt(element.css('padding-left'), 10),
                right:parseInt(element.css('padding-right'), 10)
            }
        };
    }

    function calculateInnerDistance(elementDimensions) {
        var vertical = elementDimensions.border.top + elementDimensions.border.bottom;
        vertical += elementDimensions.margin.top + elementDimensions.margin.bottom;
        vertical += elementDimensions.padding.top + elementDimensions.padding.top;

        var horizontal = elementDimensions.border.left + elementDimensions.border.right;
        horizontal += elementDimensions.margin.left + elementDimensions.margin.right;
        horizontal += elementDimensions.padding.left + elementDimensions.padding.right;

        return {
            vertical : vertical,
            horizontal : horizontal
        };
    }

    presenter.validateInstructions = function (modelInstructions) {
        var instructions = modelInstructions.split("\n");
        for(var i=0; i &lt; instructions.length; i++) {
            var instruction = instructions[i].split(' ');
            presenter.colorSquare(instruction[0], instruction[1]);
        }
    };

    presenter.colorSquareCommand = function (command) {
        presenter.colorSquare(command[0], command[1]);
    };

    presenter.colorSquare = function (x, y){
        var coordinates = x+"-"+ y,
            element = presenter.$view.find('.cell-element[coordinates="'+ coordinates +'"]');

        element.css('background-color', presenter.configuration.color);
        element.attr('colored', 'true');
    };

    presenter.resetSquareCommand = function (command) {
        presenter.resetSquare(command[0], command[1]);
    };

    presenter.resetSquare = function (x, y){
        var coordinates = x+"-"+ y,
            element = presenter.$view.find('.cell-element[coordinates="'+ coordinates +'"]');

        element.css('background-color', 'transparent');
        element.attr('colored', 'false');
    };

    presenter.run = function(view, model){
        presenterLogic(view, model);
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.createPreview = function(view, model){
        presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    function returnErrorObject(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    function presenterLogic(view, model) {
        presenter.configuration = presenter.validateModel(model);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.$view = $(view);
        viewContainer = $(view);
        gridContainerWrapper = viewContainer.find(".cellGridWrapper:first");
        gridContainer = gridContainerWrapper.find(".cellGrid:first");

        initGrid(model, true);

        if(presenter.configuration.initialDesign){
            presenter.validateInstructions(presenter.configuration.initialDesign);
        }
    }

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.ERROR_CODES = {
        E01: "Columns and rows must be a positive integer"
    };

    presenter.validateModel = function(model) {
        var validatedIsVisible = ModelValidationUtils.validateBoolean(model['Is Visible']),
            addonID = model['ID'],
            rows = ModelValidationUtils.validatePositiveInteger(model['Rows']),
            columns = ModelValidationUtils.validatePositiveInteger(model['Columns']);

        if(!rows.isValid || !columns.isValid){
            return returnErrorObject('E01');
        }

        var color = model['Color'];
        if(color == ''){
            color = 'black';
        }

        return {
            'isError' : false,
            'isVisible' : validatedIsVisible,
            'visibleByDefault' : validatedIsVisible,
            'addonID' : addonID,
            'rows' : rows.value,
            'columns' : columns.value,
            'initialDesign' : model['Initial design'],
            'color' : color
        }
    };

    presenter.getErrorObject = function (errorCode) {
        return {isValid: false, isError: true, errorCode: errorCode};
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'colorSquare' : presenter.colorSquareCommand,
            'resetSquare' : presenter.resetSquareCommand,
            'reset' : presenter.reset
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.resetAll = function () {
        presenter.$view.find('.cell-element').each(function () {
            $(this).css('background-color', 'transparent');
            $(this).attr('colored', 'false');
        });
    };

    presenter.reset = function(){
        presenter.$view.find('.cell-element').each(function () {
            if($(this).attr('colored') == 'true'){
                var coordinates = $(this).attr('coordinates').split('-');
                presenter.resetSquare(coordinates[0], coordinates[1]);
            }
        });

        if(presenter.configuration.initialDesign){
            presenter.validateInstructions(presenter.configuration.initialDesign);
        }

        presenter.setVisibility(presenter.configuration.visibleByDefault);
    };

    presenter.getState = function(){
        var coordinates = [];
        presenter.$view.find('.cell-element').each(function () {
            if($(this).attr('colored') == 'true'){
                coordinates.push($(this).attr('coordinates'));
            }
        });

        var state = {
            'coordinates' : coordinates
        };

        return JSON.stringify(state);
    };

    presenter.setState = function(state){
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsed = JSON.parse(state);

        var coordinates = parsed.coordinates;

        presenter.resetAll();

        if(coordinates){
            for(var i = 0; i &lt; coordinates.length; i++){
                var coordinate = coordinates[i].split('-');
                presenter.colorSquare(coordinate[0], coordinate[1]);
            }
        }
    };

//    presenter.getErrorCount = function(){
//    };
//    presenter.getMaxScore = function(){
//    };
//    presenter.getScore = function(){
//    };
//    presenter.setShowErrorsMode = function(){
//    };
//    presenter.setWorkMode = function(){
//    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="ProgrammingCommandPrompt" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="sceneID" nameLabel="ProgrammingCommandPrompt_property_scene_id" type="string"/>
		<property displayName="Hide Run" name="hideRun" nameLabel="ProgrammingCommandPrompt_property_hide_run" type="boolean"/>
	</model>
<css>.CommandPrompt-wrapper {
    width: 100%;
    height: 100%;

}

.CommandPrompt-wrapper :first-child {
  opacity: .99;
}

.CommandPrompt-wrapper.editor {
    position: absolute;
    width: 100%;
    height: 100%;
}

.CommandPrompt-wrapper button {
    margin-top: 10px;
}
</css><view>&lt;div class="CommandPrompt-wrapper"&gt;
  &lt;div class="editor"&gt;&lt;/div&gt;
  &lt;button class="run"&gt;Run&lt;/button&gt;
&lt;/div&gt;
</view><preview>&lt;div class="CommandPrompt-wrapper"&gt;
  &lt;div class="editor"&gt;&lt;/div&gt;
  &lt;button class="run"&gt;Run&lt;/button&gt;
&lt;/div&gt;
</preview><presenter>function AddonProgrammingCommandPrompt_create () {
    var presenter = function () {};

    presenter.ERROR_MESSAGES = {
        TTC01: "You need to fill Scene Module ID.",
    };

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }
        presenter.$view.html(errorContainer);
    };

    presenter.run = function(view, model) {
        presenter.runLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.runLogic(view, model, true);
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this, true);
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) { return; }

        presenter.configuration.sceneModule = null;
        presenter.configuration = null;
        presenter.editor.destroy();
        presenter.editor.container.remove();
        presenter.editor = null;
    };

    presenter.runLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.view = view;
        presenter.configuration = presenter.validateModel(model);

        if (!presenter.configuration.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[presenter.configuration.errorCode]);
            return;
        }

        var editor = presenter.$view.find(".editor");
        editor.css({
            width: presenter.$view.width(),
            height: presenter.$view.height()
        });
        presenter.editor = ace.edit(editor[0]);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        presenter.setRunButton();

        if (!isPreview) {
            presenter.connectHandlers();
        }

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
    };
    
    presenter.connectHandlers = function () {
        presenter.configuration.sceneModule = presenter.playerController.getModule(presenter.configuration.sceneID);
        presenter.$view.find(".run").click(function () {
            if (presenter.configuration.sceneModule !== null) {
                var code = presenter.getWorkspaceCode();
                presenter.configuration.sceneModule.executeCode(code);
            }
        });        
    };

    presenter.setCode = function Programming_Command_Prompt_set_code (code) {
        presenter.editor.setValue(code, code.length+1);
    };

    presenter.setRunButton = function () {
        if (presenter.configuration.hideRun) {
            presenter.$view.find(".run").css({
                "display": "none"
            });
        }
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'getWorkspaceCode' : presenter.getWorkspaceCode,
            'show': presenter.show,
            'hide': presenter.hide,
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getWorkspaceCode = function () {
        return presenter.editor.getValue();
    };

    presenter.validateModel = function (model) {
        var sceneID = model["sceneID"].trim();
        if (sceneID == "") {
            return {
                isValid: false,
                errorCode: "TTC01"
            };
        }

        return {
            addonID: model['ID'],
            isValid: true,
            sceneID: sceneID,
            sceneModule: null,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            defaultVisibility: ModelValidationUtils.validateBoolean(model['Is Visible']),
            hideRun: ModelValidationUtils.validateBoolean(model["hideRun"]),
        };
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (eventName == "PageLoaded") {
            if (presenter.configuration.sceneModule === null || presenter.configuration.sceneModule === undefined) {
                presenter.configuration.sceneModule = presenter.playerController.getModule(presenter.configuration.sceneID);
            }
        }
    };
    
    presenter.getState = function Programming_Command_Prompt_get_state () {
        return JSON.stringify({
            code: presenter.getWorkspaceCode(),
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function Programming_Command_Prompt_set_state (state) {
        var object = JSON.parse(state);
        presenter.setCode(object.code);
        presenter.setVisibility(object.isVisible);
    };

    presenter.reset = function () {
        presenter.setCode("");
        presenter.setVisibility(presenter.configuration.defaultVisibility);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="PseudoCode_Console" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="functionsList" nameLabel="PseudoCode_Console_property_functions_list" type="list">
            <property name="name" nameLabel="PseudoCode_Console_property_functions_name" type="string"/>
            <property name="body" nameLabel="PseudoCode_Console_property_functions_body" type="text"/>
        </property>
        <property name="methodsList" nameLabel="PseudoCode_Console_property_methods_list" type="list">
            <property name="objectName" nameLabel="PseudoCode_Console_property_object_name" type="{Number,String,Array}"/>
            <property name="methodName" nameLabel="PseudoCode_Console_property_method_name" type="string"/>
            <property name="methodBody" nameLabel="PseudoCode_Console_property_method_body" type="text"/>
        </property>
        <property name="default_aliases" nameLabel="PseudoCode_Code_property_default_aliases" type="staticlist">
            <property displayName="Begin" name="begin" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="End" name="end" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Variable" name="variable" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="For" name="for" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="While" name="while" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="From" name="from" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="To" name="to" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Do" name="do" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Program" name="program" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Or" name="or" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="And" name="and" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="If" name="if" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Then" name="then" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Else" name="else" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Case" name="case" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Option" name="option" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Function" name="function" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Return" name="return" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Array" name="array_block" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="Downto" name="down_to" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
            <property displayName="By" name="by" type="staticrow">
                <property name="name" nameLabel="Pseudo_Console_property_functions_name" type="string"/>
            </property>
        </property>
        <property name="exceptionsTranslation" nameLabel="PseudoCode_Code_property_exception_translations_list" type="staticlist">
            <property displayName="The instruction '{0}' has already been defined" name="InstructionIsDefinedException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Cast exception '{0}' to type: '{1}'" name="CastErrorException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Exception ({0}): Value at index {1} is not defined" name="GetErrorException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Exception ({0}): index {1} is out of bounds" name="IndexOutOfBoundsException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="To few arguments for function '{0}' (expected at least: {1} arguments)" name="ToFewArgumentsException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Undefined method '{0}'" name="MethodNotFoundException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Usage of undefined variable '{0}' in function '{1}'" name="UndefinedVariableNameException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Usage of undefined function '{0}' in function '{1}'" name="UndefinedFunctionNameException" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
            <property displayName="Unexpected identifier" name="UnexpectedIdentifier" type="staticrow">
                <property name="translation" nameLabel="Pseudo_Console_property_translation_name" type="string"/>
            </property>
        </property>
        <property name="isNotActivity" nameLabel="PseudoCode_Console_property_is_not_activity" type="boolean"/>
        <property name="runUserCode" nameLabel="PseudoCode_Console_property_run_user_code" type="boolean"/>
        <property name="runParameters" nameLabel="PseudoCode_Console_property_run_parameters" type="list">
            <property name="value" nameLabel="PseudoCode_Console_property_parameters_value" type="string"/>
        </property>
        <property name="maxTimeForAnswer" nameLabel="PseudoCode_Console_property_answer_max_timeout" type="string"/>
        <property name="answerCode" nameLabel="PseudoCode_Console_property_answer_code" type="text"/>
        <property name="mathRound" nameLabel="PseudoCode_Console_property_math_round" type="string"/>
        <property name="consoleAvailableInput" nameLabel="PseudoCode_Console_property_console_available_input" type="{All,Natural numbers,Real numbers,Letters only}"/>

    </model>
<css>.pseudoConsole-console-cursor {
    background-color: #666;
}

.addon-PseudoCode_Console-console {
    width: 100%;
    height: 100%;
}

.pseudoConsole-console-right-element {
    position: relative;
}

.pseudoConsole-console-textarea {
    width: 1px;
    height: 1px;
    color: transparent;
    background-color: transparent;
    border: none;
    position: absolute;
    opacity: 0.000001;
}

.addon-PseudoCode_Console-wrapper {
    width: 100%;
    height: 100%;
    overflow: auto;
}

.pseudoConsole-console-cursor-active {
    margin-right: -0.5em;
}
</css><view>&lt;div class="addon-PseudoCode_Console-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="addon-PseudoCode_Console-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module &amp;&amp; module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = 7);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */,
/* 1 */,
/* 2 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.generateExecuteObject = generateExecuteObject;
exports.generateJumpInstruction = generateJumpInstruction;
var TYPES = exports.TYPES = {
    EXECUTE: 1,
    JUMP: 2
};

/**
 * Generate code executed by addon.
 * @param  {String} code
 * @param  {String} label set label for goto instruction
 * @param  {Boolean} [isAsync] async instructions cant be merged and is optional
 */
function generateExecuteObject(code, label, isAsync) {
    return {
        code: code,
        type: TYPES.EXECUTE,
        label: label,
        isAsync: isAsync || false
    };
}

function generateJumpInstruction(code, toLabel) {
    return {
        code: code,
        toLabel: toLabel,
        type: TYPES.JUMP
    };
}

/***/ }),
/* 3 */,
/* 4 */,
/* 5 */,
/* 6 */,
/* 7 */
/***/ (function(module, exports, __webpack_require__) {

var _languageDefinition = __webpack_require__(8);

var _definedObjects = __webpack_require__(9);

var _definedExceptions = __webpack_require__(10);

var _languageCodeGenerators = __webpack_require__(11);

var _console = __webpack_require__(12);

var _machine = __webpack_require__(13);

var _validation = __webpack_require__(14);

var _utils = __webpack_require__(15);

/**
 * Teoria:
 * http://wazniak.mimuw.edu.pl/index.php?title=Podstawy_kompilator%C3%B3w
 * Check comments if you want to add OOP to language.
 */
function AddonPseudoCode_Console_create() {
    var presenter = function presenter() {};

    /**
     * Each object in pseudocode console must be created by this mock.
     */
    presenter.objectMocks = {};
    presenter.bnf = _languageCodeGenerators.CODE_GENERATORS;
    presenter.exceptions = null;
    presenter.console = _console.UserConsole;
    presenter.codeExecutor = _machine.codeExecutor;
    presenter.validateModel = _validation.validateModel;
    presenter.validateFunction = _validation.validateFunction;
    presenter.validateAnswer = _validation.validateAnswer;
    presenter.validateFunctions = _validation.validateFunctions;
    presenter.validateAliases = _validation.validateAliases;
    presenter.validateMethod = _validation.validateMethod;

    //This object will be passed to instruction as scope
    presenter.objectForInstructions = {
        calledInstructions: {
            for: 0,
            while: 0,
            doWhile: 0,
            if: 0,
            case: 0
        }, //Object with calculated each built in instruction call e.g. for, while,
        data: {}
    };

    presenter.state = {
        console: null,
        functions: {}, //Functions defined by user
        codeGenerator: null, //Generator code to execute from string.
        wasChanged: false, //If code was changed and addon must recalculate score
        lastScore: 0, //Last score, we dont need to recalculate score if user dont run code
        lastUsedCode: [], //Compiled code which was last used,
        definedByUserFunctions: [], //Functions defined by user
        variablesAndFunctionsUsage: {}, //Functions and variables used by user each element contains: {defined: [], args: [], vars: [], fn: []},
        addonWrapper: null,
        _disabled: false,
        _wasExecuted: false
    };

    presenter.configuration = {
        aliases: {
            "begin": "begin",
            "do": "do",
            "end": "end",
            "for": "for",
            "from": "from",
            "to": "to",
            "variable": "variable",
            "program": "program",
            "while": "while",
            "or": "or",
            "and": "and",
            "if": "if",
            "then": "then",
            "else": "else",
            "case": "case",
            "option": "option",
            "function": "function",
            "return": "return",
            "array_block": "array",
            "down_to": "downto",
            "by": "by"
        },
        isValid: false,
        addonID: null,
        isActivity: false,
        isVisibleByDefault: false,
        functions: [],
        answer: null,
        methods: [],
        round: 20,
        availableConsoleInput: "All",
        exceptionTranslations: {}
    };

    presenter.ERROR_CODES = {
        "FN01": "Defined function name must match to ^[A-Za-z_][a-zA-Z0-9_]*$",
        "FN02": "Defined function must have unique name",
        "FN03": "Defined function overrides built in alias",
        "AN01": "Defined alias name must match to ^[A-Za-z_][a-zA-Z0-9_]*$",
        "AN02": "Multiple aliases got the same name",
        "JS01": "Java Script code in mdefined ethod is not valid.",
        "JS02": "Java Script code in defined function is not valid",
        "ER01": "Math precision value must be an integer",
        "ER02": "Math precision value must be bigger than 0",
        "ER03": "Math precision value cannot be greater than 20",
        "IP01": "Max time for answer must be float number in range 0 to 10 excluding 0",
        "IP02": "Answer code must be valid JS code"
    };

    presenter.availableInputsInConsole = {
        "All": function All() {
            return true;
        },
        "Natural numbers": function NaturalNumbers(value, wholeValue) {
            return (0, _utils.isDigit)(value);
        },
        "Letters only": function LettersOnly(value, wholeValue) {
            return (0, _utils.isLetter)(value);
        },
        "Real numbers": function RealNumbers(value, wholeValue) {
            return (/^-?[0-9]*\.?[0-9]*$/g.test(wholeValue)
            );
        }
    };

    presenter.CLASS_LIST = {
        "correct": "pseudo-code-console-correct",
        "wrong": "pseudo-code-console-wrong"
    };

    presenter.originalDisplay = 'block';

    presenter.setPlayerController = function presenter_setPlayerController(controller) {
        presenter.state.playerController = controller;
        presenter.state.eventBus = presenter.state.playerController.getEventBus();
        presenter.state.eventBus.addEventListener('ShowAnswers', this);
        presenter.state.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.killMachine = {}; // Object which contains all machines with kill machine function

    presenter.killAllMachines = function presenter_killAllMachines() {
        var id = void 0;

        for (id in presenter.killMachine) {
            if (presenter.killMachine.hasOwnProperty(id)) {
                presenter.killMachine[id]();
            }
        }
    };

    presenter.run = function presenter_run(view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function presenter_createPreview(view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.initializeGrammar = function presenter_initializeGrammar() {
        presenter.state.codeGenerator = (0, _languageDefinition.getLanguageParser)({
            presenter: presenter,
            aliases: presenter.configuration.aliases
        });
    };

    /**
     * Before each user code call, this object should be initialized
     * @param  {Object} [consoleMock] optional argument for console
     */
    presenter.initializeObjectForCode = function presenter_initializeObjectForCode(consoleMock) {
        presenter.objectForInstructions = {
            calledInstructions: {
                for: 0,
                while: 0,
                doWhile: 0,
                if: 0,
                case: 0
            },
            data: {}
        };
        presenter.objectForInstructions.console = consoleMock || presenter.state.console;
        presenter.state.definedByUserFunctions = [];
    };

    presenter.getInputChecker = function () {
        return presenter.availableInputsInConsole[presenter.configuration.availableConsoleInput];
    };

    presenter.initializeConsole = function presenter_initializeConsole() {
        presenter.state.console = new presenter.console(presenter.state.$view.find(".addon-PseudoCode_Console-wrapper"), {
            inputChecker: presenter.getInputChecker()
        });

        var originalReadLine = presenter.state.console.ReadLine,
            originalReadChar = presenter.state.console.ReadChar;

        /**
         * Because console is asynchronous but pseudocode console is synchronous we must wrap user callback in console.
         * Before executing original console function we must stop machine which is executing this code and when user enters input then we resume machine
         * pauseIns and nextIns are set while executing each command (see dispatchForBuiltInFunctions which is calling wrapMethodOrFunctionWithBuiltInCode code)
         * @param callback {Function}
         */
        presenter.state.console.ReadLine = function console_read_line_override(callback) {
            presenter.state.console.pauseIns();
            originalReadLine.call(presenter.state.console, function (input) {
                callback.call(this, input);
                presenter.state.console.nextIns();
            });
        };

        /**
         * Like ReadLine
         * @param callback {Function}
         */
        presenter.state.console.ReadChar = function console_read_char_override(callback) {
            presenter.state.console.pauseIns();
            originalReadChar.call(presenter.state.console, function (input) {
                callback.call(this, input);
                presenter.state.console.nextIns();
            });
        };
    };

    presenter.completeObjectsMethods = function presenter_completeObjectsMethods() {
        presenter.objectMocks = (0, _definedObjects.getDefinedObjects)({
            round: presenter.configuration.round,
            exceptions: presenter.exceptions
        });

        presenter.configuration.methods.forEach(function (method) {
            if (method.objectName !== "" &amp;&amp; method.methodName !== "") {
                presenter.objectMocks[method.objectName].__methods__[method.methodName] = {
                    native: true,
                    jsCode: method.function
                };
            }
        });
    };

    presenter.initialize = function presenter_initialize(view, model, isPreview) {
        presenter.configuration = presenter.validateModel(model, presenter.configuration.aliases);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }
        presenter.state.$view = $(view);
        presenter.state.view = view;

        var display = presenter.state.$view.css('display');
        if (display != null &amp;&amp; display.length &gt; 0) {
            presenter.originalDisplay = display;
        }

        presenter.state.addonWrapper = presenter.state.$view.find(".addon-PseudoCode_Console-wrapper");
        if (!isPreview) {
            presenter.initializeExceptions();
            presenter.initializeConsole();
            presenter.initializeObjectForCode();
            presenter.initializeGrammar();
            presenter.completeObjectsMethods();
            MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.state.view);
            MutationObserverService.setObserver();
        }
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
    };

    presenter.initializeExceptions = function () {
        presenter.exceptions = new _definedExceptions.EXCEPTIONS(presenter.configuration.exceptionTranslations);
    };

    presenter.stop = function presenter_stop() {
        presenter.state.console.Reset();
        presenter.killAllMachines();
    };

    presenter.onEventReceived = function presenter_onEventReceived(eventName) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.executeCommand = function presenter_executeCommand(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'stop': presenter.stop,
            'showAnswers': presenter.showAnswers,
            'hideAnswers': presenter.hideAnswers
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.showAnswers = function presenter_showAnswers() {
        presenter.setWorkMode();
        presenter.state._disabled = true;
        presenter.state.console.disable();
    };

    presenter.hideAnswers = function presenter_hideAnswers() {
        presenter.state._disabled = false;
        presenter.state.console.enable();
    };

    presenter.destroy = function presenter_destroy(event) {
        if (event.target !== presenter.state.view) {
            return;
        }

        if (presenter.state.console) {
            presenter.state.console.destroy();
        }
    };

    presenter.setVisibility = function presenter_setVisibility(isVisible) {
        presenter.state.$view.css('visibility', isVisible ? 'visible' : 'hidden');
        presenter.state.$view.css('display', isVisible ? presenter.originalDisplay : 'none');

        presenter.state.isVisible = isVisible;
    };

    presenter.show = function presenter_show() {
        presenter.setVisibility(true);
    };

    presenter.hide = function presenter_hide() {
        presenter.setVisibility(false);
    };

    presenter.reset = function presenter_reset() {
        presenter.state._wasExecuted = false;
        presenter.setWorkMode();
        presenter.hideAnswers();
        presenter.killAllMachines();
        presenter.state.console.Reset();
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.state.console.enable();
        presenter.state.lastUsedCode = [];
        presenter.state.wasChanged = true;
    };

    presenter.setShowErrorsMode = function presenter_setShowErrorsMode() {
        presenter.hideAnswers();
        presenter.state._disabled = true;

        if (!presenter.state._wasExecuted) {
            return;
        }

        if (presenter.configuration.isActivity) {
            if (presenter.getScore() === 1) {
                presenter.state.addonWrapper[0].classList.add(presenter.CLASS_LIST.correct);
            } else {
                presenter.state.addonWrapper[0].classList.add(presenter.CLASS_LIST.wrong);
            }
        }
        presenter.state.console.disable();
    };

    presenter.setWorkMode = function presenter_setWorkMode() {
        presenter.state._disabled = false;

        presenter.state.addonWrapper[0].classList.remove(presenter.CLASS_LIST.correct);
        presenter.state.addonWrapper[0].classList.remove(presenter.CLASS_LIST.wrong);
        presenter.state.console.enable();
    };

    presenter.setState = function presenter_setState(stateString) {
        var state = JSON.parse(stateString);

        presenter.setVisibility(state.isVisible);
        presenter.state.lastScore = state.score;
        presenter.state._wasExecuted = state._wasExecuted;
    };

    presenter.getState = function presenter_getState() {
        var state = {
            isVisible: presenter.state.isVisible,
            score: presenter.state.lastScore,
            _wasExecuted: presenter.state._wasExecuted //Added later and can be false
        };

        return JSON.stringify(state);
    };

    presenter.evaluateScoreFromLastOutput = function presenter_evaluateScoreFromLastOutput() {
        try {
            if (presenter.configuration.answer.answerCode.call(presenter.objectForInstructions)) {
                return 1;
            }

            return 0;
        } catch (e) {
            return 0;
        }
    };

    presenter.generateConsoleMock = function presenter_generateConsoleMock(input) {
        var actualInputIndex = 0;
        return {
            Reset: function Reset() {},

            ReadLine: function ReadLine(callback) {
                var actualInput = input[actualInputIndex];
                if (actualInput !== null) {
                    callback.call(presenter.state.console, actualInput);
                    actualInputIndex += 1;
                }
            },

            ReadChar: function ReadChar(callback) {
                var actualInput = input[actualInputIndex];
                if (actualInput !== null) {
                    callback.call(presenter.state.console, actualInput);
                    actualInputIndex += 1;
                }
            },
            Write: function Write() {}
        };
    };

    presenter.evaluateScoreFromUserCode = function presenter_evaluateScoreFromUserCode() {
        var code = presenter.state.lastUsedCode,
            objectForInstructionsSaved = presenter.objectForInstructions,
            score = void 0;

        presenter.initializeObjectForCode(presenter.generateConsoleMock(presenter.configuration.answer.parameters));
        presenter.codeExecutor(code, true, presenter);
        score = presenter.evaluateScoreFromLastOutput();

        presenter.objectForInstructions = objectForInstructionsSaved;

        return score;
    };

    presenter.evaluateScore = function presenter_evaluateScore() {
        if (presenter.configuration.answer.runUserCode) {
            return presenter.evaluateScoreFromUserCode();
        }

        return presenter.evaluateScoreFromLastOutput();
    };

    function sendValueChangedEvent(eventData) {
        if (presenter.state.eventBus !== null) {
            presenter.state.eventBus.sendEvent('ValueChanged', eventData);
        }
    }

    function sendScoreChangedEvent(score) {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': score
        };
        sendValueChangedEvent(eventData);
    }

    presenter.getScore = function presenter_getScore() {
        if (!presenter.state.wasChanged) {
            return presenter.state.lastScore;
        }

        var score = 0;
        if (presenter.configuration.isActivity) {
            score = presenter.evaluateScore();

            presenter.state.lastScore = score;
            presenter.state.wasChanged = false;

            sendScoreChangedEvent(score);
        }

        return score;
    };

    presenter.getMaxScore = function presenter_getMaxScore() {
        if (presenter.configuration.isActivity) {
            return 1;
        }

        return 0;
    };

    presenter.getErrorCount = function presenter_getErrorCount() {
        if (presenter.configuration.isActivity) {
            return 1 - presenter.getScore();
        }

        return 0;
    };

    presenter.getExcludedNames = function presenter_getExcludedNames() {
        var i = void 0,
            excludedNames = {};

        for (i in presenter.configuration.aliases) {
            if (presenter.configuration.aliases.hasOwnProperty(i)) {
                excludedNames[presenter.configuration.aliases[i]] = true;
            }
        }

        for (i in presenter.configuration.functions) {
            if (presenter.configuration.functions.hasOwnProperty(i)) {
                excludedNames[i] = true;
            }
        }

        return excludedNames;
    };

    /**
     * If user defined function which was defined then throw error
     */
    presenter.multiDefineInstructionChecker = function presenter_multiDefineInstructionChecker() {
        var i = void 0,
            userFunctionName = "",
            excludedNames = presenter.getExcludedNames();

        for (i = 0; i &lt; presenter.state.definedByUserFunctions.length; i += 1) {
            userFunctionName = presenter.state.definedByUserFunctions[i];
            if (!excludedNames[userFunctionName]) {
                excludedNames[userFunctionName] = true;
            } else {
                throw presenter.exceptions.InstructionIsDefinedException(userFunctionName);
            }
        }
    };

    /**
     * User calls undefined function
     * @param  {{defined: String[], args: String[], vars:String [], fn: String[]}} functionData
     * @param  {String} functionName
     */
    presenter.undefinedUsageForFunctionChecker = function presenter_undefinedUsageForFunctionChecker(functionData, functionName) {
        var usedVariableName = "",
            usedFunctionName = "",
            i = void 0,
            excludedNames = presenter.getExcludedNames();

        for (i = 0; i &lt; functionData.vars.length; i += 1) {
            usedVariableName = functionData.vars[i];
            if ($.inArray(usedVariableName, functionData.defined) === -1 &amp;&amp; $.inArray(usedVariableName, functionData.args) === -1) {
                throw presenter.exceptions.UndefinedVariableNameException(usedVariableName, functionName);
            }
        }

        for (i = 0; i &lt; functionData.fn.length; i += 1) {
            usedFunctionName = functionData.fn[i];
            if (!excludedNames[usedFunctionName] &amp;&amp; $.inArray(usedFunctionName, presenter.state.definedByUserFunctions) === -1) {
                throw presenter.exceptions.UndefinedFunctionNameException(usedFunctionName, functionName);
            }
        }
    };

    /**
     * Check if user uses not defined variable or instruction
     */
    presenter.undefinedInstructionOrVariableChecker = function presenter_undefinedInstructionOrVariableChecker() {
        var i = void 0,
            usedVariablesAndFunctions = {};

        for (i in presenter.state.variablesAndFunctionsUsage) {
            if (presenter.state.variablesAndFunctionsUsage.hasOwnProperty(i)) {
                usedVariablesAndFunctions = presenter.state.variablesAndFunctionsUsage[i];
                presenter.undefinedUsageForFunctionChecker(usedVariablesAndFunctions, i);
            }
        }
    };

    presenter.checkCode = function presenter_checkCode() {
        presenter.multiDefineInstructionChecker();
        presenter.undefinedInstructionOrVariableChecker();
    };

    presenter.executeCode = function presenter_executeCode(code) {
        if (!presenter.configuration.isValid) {
            return;
        }

        if (presenter.state._disabled) {
            return;
        }

        presenter.state.variablesAndFunctionsUsage = {};
        presenter.state.wasChanged = true;
        presenter.state._wasExecuted = true;
        presenter.state.lastUsedCode = [];
        presenter.initializeObjectForCode();
        try {
            presenter.state.console.Reset();
            var executableCode = presenter.state.codeGenerator.parse(code);
            presenter.checkCode();

            presenter.state.lastUsedCode = executableCode;
            presenter.stop();

            presenter.codeExecutor(executableCode, false, presenter);
        } catch (e) {
            if (e.name !== "Error") {
                presenter.state.console.Write(e.message + "\n", 'program-error-output');
            } else {
                presenter.state.console.Write(presenter.configuration.exceptionTranslations.UnexpectedIdentifier || "Unexpected identifier\n", 'program-error-output');
            }
        }
    };

    return presenter;
}

window.AddonPseudoCode_Console_create = AddonPseudoCode_Console_create;

/***/ }),
/* 8 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.getLanguageParser = getLanguageParser;
/**
 * OOP:
 *      Each variable is object, with struct as:
 *      {
 *          value: valueOfObject,
 *          methods: {
 *              methodName: {isBuiltIn: true|false, labelToJump: string|null, jsCode: string|null}
 *          },
 *          type: typeNameAsString,
 *          parent: object to parent or null
 *      }
 *      New object is created by function which will returns empty object.
 *      Each object can contains class methods, which name will start with __. (Concept: user can override this methods by built prefix.)
 *
 *      Calling built in methods:
 *          Check Object call manager for more information
 *
 *      Defined by user (concept):
 *          Set as actual context object value, and jump to labelToJump
 *
 *          Example class:
 *          class A
 *               field zmienna
 *
 *               method getName (tekst)
 *               begin
 *                   print()
 *               end
 *
 *               built method add ()    %As override class method in add%
 *               begin
 *                   return "A"
 *               end
 *
 *
 *           endClass
 *
 *          check object call manager for more information
 *
 */

function getJISONGrammar() {
    return {
        "lex": {
            "options": {
                flex: true
            },
            "rules": [
            //String section, if lexer will catch " without string condition, then start condition string. While this condition, only rules with ["string"] will be in use
            //and this condition will be turned off if " will be catch with start condition
            ["[\"]", "this.begin('string'); return 'START_STRING'"], [["string"], "[^\"\\\\]", "return 'STRING';"], [["string"], "[\\n]", "return 'NEWLINE_IN_STRING';"], [["string"], "\\\\.", "return 'STRING'"], // match \. &lt;- escaped characters"
            [["string"], "$", "return 'EOF_IN_STRING';"], [["string"], "[\"]", "this.popState(); return 'END_STRING';"],
            //Words between |&lt;name&gt;| will be replaced by values from configuration
            ["|begin|", "return 'BEGIN_BLOCK';"], ["|end|", "return 'END_BLOCK';"], ["|program|", "return 'PROGRAM';"], ["|variable|", "return 'VARIABLE_DEF';"], ["|for|", "return 'FOR';"], ["|from|", "return 'FROM';"], ["|to|", "return 'TO';"], ["|do|", "return 'DO';"], ["|or|", "return 'OR';"], ["|and|", "return 'AND';"], ["|while|", "return 'WHILE';"], ["|if|", "return 'IF';"], ["|then|", "return 'THEN';"], ["|else|", "return 'ELSE';"], ["|case|", "return 'CASE';"], ["|option|", "return 'OPTION';"], ["|function|", "return 'FUNCTION';"], ["|return|", "return 'RETURN';"], ["|array_block|", "return 'ARRAY_DEF';"], ["|down_to|", "return 'DOWNTO';"], ["|by|", "return 'BY';"], ["\\n+", "return 'NEW_LINE';"], ["$", "return 'EOF';"], ["[0-9]+(?:\\.[0-9]+)?\\b", "return 'NUMBER';"], ["&lt;=", "return '&lt;=';"], ["&gt;=", "return '&gt;=';"], ["!=", "return '!=';"], ["==", "return '==';"], ["&lt;", "return '&lt;';"], ["&gt;", "return '&gt;';"], ["\\*", "return '*';"], ["\\/_", "return 'DIV_FLOOR';"], ["\\/", "return '/';"], ["-", "return '-';"], ["\\+", "return '+';"], ["%", "return '%';"], ["\\(", "return '(';"], ["\\)", "return ')';"], ["[A-Za-z][a-zA-Z0-9_]*", "return 'STATIC_VALUE';"], ["\\[", "return '[';"], ["\\]", "return ']';"], [",", "return 'COMMA';"], ["\\.", "return 'DOT';"], ["=", "return '=';"], ["[ \f\r\t\x0B\u200B\xA0\u1680\u200B\u180E\u2000\u200B\u2001\u2002\u200B\u2003\u2004\u200B\u2005\u2006\u200B\u2007\u2008\u200B\u2009\u200A\u200B\u2028\u2029\u200B\u2028\u2029\u200B\u202F\u205F\u200B\u3000]", "/* IGNORE SPACES */"], [".", "return 'NOT_MATCH';"]],

            //Each conditions used by lexer must be defined there
            "startConditions": {
                string: 1
            }
        },

        //Operators order
        "operators": [//Be sure, you added operators here to avoid problems with conflicts
        ["left", "OR", "AND"], ["left", "&lt;=", "&gt;=", "&lt;", "&gt;", "!=", "=="], ["left", "+", "-"], ["left", "*", "/", "DIV_FLOOR", "%"], ["left", "(", ")"], ["lefr", "DOT"], ["left", "[", "]"], ["left", "UMINUS"], ["right", "IF", "ELSE", "THEN"], ["right", "CASE", "OPTION"]],
        "bnf": {
            //entry point
            "expressions": [
            //Code executor will stop when will receive undefined to execute.
            ["functions program_name section_list code_block", "return {sections: '', code: ($3 || []).concat($4).concat(undefined).concat($1).concat(undefined).concat(yy.presenterContext.bnf['getObjectCallManager']())};($2 || '') + ($3 || '');"]],

            "functions": ["", ["functions_list", "$$ = $1;"]],

            "functions_list": [["function", "$$ = $1;"], ["functions_list function", "$$ = $1.concat($2);"]],

            "function": [["function_declaration ( function_arguments ) end_line section_list code_block", "$$ = yy.presenterContext.bnf['function'](yy, $1, $3 || [], $6, $7);"]],

            "function_declaration": [["FUNCTION STATIC_VALUE", "$$ = yy.presenterContext.bnf['function_declaration'](yy, $2);"]],

            "function_arguments": ["", ["function_arguments_list", "$$ = $1 || [];"]],

            "function_arguments_list": [["STATIC_VALUE", "$$ = [$1];"], ["function_arguments_list COMMA STATIC_VALUE", "$1.push($3); $$ = $1;"]],

            "program_name": [["program_const STATIC_VALUE end_line", "$$ = yy.presenterContext.bnf['program_name'](yy, $2); "]],

            "program_const": [["PROGRAM", "$$ = '';"]],

            "section_list": ["", ["section_list section", "$$ = ($1 || []).concat($2);"]],

            "section": [["var_section", "$$ = $1;"], ["array_section", "$$ = $1;"]],

            "array_section": [["ARRAY_DEF array_list NEW_LINE", "$$ = $2 || []"]],

            "array_list": [["array_definition", "$$ = $1"], ["array_list COMMA array_definition", "$$ = $1"]],

            "array_definition": [["STATIC_VALUE [ NUMBER ] array_start_value", "$$ = yy.presenterContext.bnf['array'](yy, $1, $3, $5);"]],

            "array_start_value": ["", [" = [ array_start_entries ]", "$$ = $3"]],

            "array_start_entries": [["array_start_entry", "$$ = [$1]"], ["array_start_entries COMMA array_start_entry", "$1.push($3); $$ = $1;"]],

            "array_start_entry": [["operation", "$$ = $1"]],

            "var_section": [["variable_def_const var_list end_line", "$$ = $2;"]],

            "variable_def_const": [["VARIABLE_DEF", "$$ = '';"]],

            "var_list": [["var", "$$ = $1;"], ["var_list comma_separator var", "$$ = $1.concat($3);"]],

            "comma_separator": [["COMMA", "$$ = '';"]],

            "var": [["STATIC_VALUE", "$$ = [yy.presenterContext.bnf['var'](yy, yytext)];"], ["STATIC_VALUE = operation", "$$ = yy.presenterContext.bnf['var_start_value'](yy, $1, $3);"]],

            "code_block": [["begin_block instructions end_block", "$$ = $2 || [];"]],

            "code_block_or_instruction": [["code_block", "$$ = $1 || [];"], ["instruction", "$$ = $1 || [];"]],

            "begin_block": [["BEGIN_BLOCK end_line", "$$ = '';"]],

            "end_block": [["END_BLOCK end_line", "$$ = '';"]],

            "instructions": ["", ["instruction_list", "$$ = $1;"]],

            "instruction_list": [["instruction", "$$ = $1;"], ["instruction_list instruction", "$$ = $1.concat($2);"]],

            "instruction": [['for_instruction', '$$ = $1;'], ['while_instruction', '$$ = $1;'], ['do_while_instruction', '$$ = $1;'], ["assign_value", "$$ = $1;"], ["if_instruction", "$$ = $1"], ["case_instruction", "$$ = $1;"], ["RETURN operation end_line", "$$ = yy.presenterContext.bnf['return_value'](yy, $2);"]],

            "case_instruction": [["CASE variable_get end_line case_options", "$$ = yy.presenterContext.bnf['case']($2, $4);"]],

            "case_options": [["case_option", "$$ = $1;"], ["case_options case_option", "$$ = $1.concat($2);"]],

            "case_option": [["OPTION case_operations THEN end_line code_block_or_instruction", "$$ = yy.presenterContext.bnf['case_option']($2, $5);"]],

            "case_operations": [["operation", "$$ = [$1]"], ["case_operations COMMA operation", "$1.push($3); $$ = $1"]],

            "number_with_minus": [["number_value", "$$ = $1"], ["- number_value", "$$ = ($2 * -1);"]],

            "if_instruction": [["IF operation THEN end_line code_block_or_instruction", "$$ = yy.presenterContext.bnf['if_instruction']($2, $5);"], ["IF operation THEN end_line code_block_or_instruction ELSE end_line code_block_or_instruction", "$$ = yy.presenterContext.bnf['if_else_instruction']($2, $5, $8);"]],

            "assign_value": [['operation [ operation ] = operation end_line', "$$ = yy.presenterContext.bnf['assign_array_value']($1, $3, $6);"], ['STATIC_VALUE = operation end_line', "$$ = yy.presenterContext.bnf['assign_value_1'](yy, $1, $3);"], ['operation end_line', "$$ = yy.presenterContext.bnf['assign_value_2']($1);"]],

            "do_while_instruction": [["do_while_header end_line code_block_or_instruction do_while_checker", "$$ = $1.concat($3).concat($4);"]],

            "do_while_header": [["DO", "$$ = yy.presenterContext.bnf['do_while_header'](yy);"]],

            "do_while_checker": [["WHILE operation end_line", "$$ = yy.presenterContext.bnf['do_while_exiter'](yy, $2);"]],

            "while_instruction": [["while_header end_line code_block_or_instruction", "var endBlock = yy.presenterContext.bnf['while_exiter'](yy); $$ = $1.concat($3).concat(endBlock);"]],

            "while_header": [["WHILE operation DO", "$$ = $$ = yy.presenterContext.bnf['while_header'](yy, $2);"]],

            "for_instruction": [["for_value_header end_line code_block_or_instruction", "$$ = $1.concat($3).concat(yy.presenterContext.bnf['for_exiter'](yy));"]],

            "for_value_header": [["FOR STATIC_VALUE FROM operation TO operation BY NUMBER DO", "$$ = yy.presenterContext.bnf['for_value_header'](yy, $2, $4, $6, $8, '&lt;=');"], ["FOR STATIC_VALUE FROM operation TO operation DO", "$$ = yy.presenterContext.bnf['for_value_header'](yy, $2, $4, $6, 1, '&lt;=');"], ["FOR STATIC_VALUE FROM operation DOWNTO operation DO", "$$ = yy.presenterContext.bnf['for_value_header'](yy, $2, $4, $6, -1, '&gt;=');"], ["FOR STATIC_VALUE FROM operation DOWNTO operation BY NUMBER DO", "$$ = yy.presenterContext.bnf['for_value_header'](yy, $2, $4, $6, $8 * -1, '&gt;=');"]],

            "static_value_or_number": [["STATIC_VALUE", "$$ = yy.presenterContext.bnf['for_argument'](yy, yytext);"], ["NUMBER", "$$ = Number(yytext);"]],

            "arguments": ["", ["arguments_list", "$$ = $1;"]],

            "arguments_list": [["argument", "$$ = [$1];"], ["arguments_list COMMA argument", "$1.push($3); $$ = $1;"]],

            "argument": [["operation", "$$ = $1;"]],

            "string_value": [["START_STRING string_chars END_STRING", "$$ = yy.presenterContext.bnf['string_value'](yy, $2);"]],

            "string_chars": ["", ["string_char", "$$ = $1"]],

            "string_char": [["STRING", "$$ = $1;"], ["string_char STRING", "$$ = $1 + $2"]],

            "end_line": [["new_line_list", "$$='';"]],

            "new_line_list": [["EOF", "$$='';"], ["NEW_LINE", "$$='';"], ["new_line_list NEW_LINE", "$$='';"], ["new_line_list EOF", "$$ = '';"]],

            "operation": [["STATIC_VALUE ( arguments )", "$$ = yy.presenterContext.bnf['function_call'](yy, $1, $3);"], ["operation + operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__add__');"], ["operation - operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__sub__');"], ["operation * operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__mul__');"], ["operation DIV_FLOOR operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__div_full__');"], ["operation / operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__div__');"], ["operation % operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__mod__');"], ["operation &lt;= operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__le__');"], ["operation &gt;= operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__ge__');"], ["operation &gt; operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__gt__');"], ["operation &lt; operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__lt__');"], ["operation != operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__neq__');"], ["operation == operation", "$$ = yy.presenterContext.bnf['generateOperationCode'](yy, $1, $3, '__eq__');"], ["operation OR operation", "$$ = yy.presenterContext.bnf['generateOptimizedOrOperationCode'](yy, $1, $3);"], ["operation AND operation", "$$ = yy.presenterContext.bnf['generateOptimizedAndOperationCode'](yy, $1, $3);"], ["( operation )", "$$ = $2"], ["- operation", "$$ = yy.presenterContext.bnf['generateMinusOperation']($2);", { "prec": "UMINUS" }], ["operation DOT STATIC_VALUE ( arguments )", "$$ = yy.presenterContext.bnf['method_call']($3, $5 || [], $1);"], ["number_value", "$$ = $1"], ["variable_get", "$$ = $1"], ["string_value", "$$ = $1"]],

            "variable_get": [["STATIC_VALUE", "$$ = yy.presenterContext.bnf['argument'](yy, yytext);"], ["operation [ operation ]", "$$ = yy.presenterContext.bnf['array_get']($1, $3);"]],

            "number_value": [["NUMBER", "$$ = yy.presenterContext.bnf['number_value'](yy, yytext);"]]
        }
    };
}

function getWordBetweenHorizontalLine(word) {
    if (word.indexOf("|") &gt; -1 &amp;&amp; word.lastIndexOf("|") !== word.indexOf("|")) {
        return word.substring(1, word.length - 1);
    }
    return null;
}

/**
 * @param {{presenter: Object, aliases: {}}}config
 */
function getLanguageParser(config) {
    var grammar = getJISONGrammar(),
        rules = grammar.lex.rules,
        aliases = config.aliases;

    for (var i = 0; i &lt; rules.length; i += 1) {
        var rule = rules[i][0];
        var word = getWordBetweenHorizontalLine(rule);
        if (word !== null) {
            //We want to find words between "$" and replace them with aliases
            if (aliases.hasOwnProperty(word)) {
                rules[i][0] = aliases[word];
            }
        }
    }

    var parser = new Jison.Parser(grammar);
    parser.yy.presenterContext = config.presenter;
    parser.yy.labelsStack = [];
    parser.yy.functionNames = [];
    return parser;
}

/***/ }),
/* 9 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.objectMocksMethodArgumentsDispatcherDecorator = objectMocksMethodArgumentsDispatcherDecorator;
exports.getDefinedObjects = getDefinedObjects;
/**
 * Arguments dispatcher for methods. Before calling method get object and arguments from stack and convert it to js call with arguments
 * @param fn {Function}
 * @returns {Function}
 */
function objectMocksMethodArgumentsDispatcherDecorator(fn) {
    return function () {
        var builtIn = {
            console: arguments[0].console,
            data: arguments[0].data,
            objects: arguments[1],
            retVal: arguments[4]
        };
        builtIn.console.nextIns = arguments[2];
        builtIn.console.pauseIns = arguments[3];
        var args = Array.prototype.slice.call(arguments, 5);

        args.push(builtIn);

        builtIn.retVal.value = fn.apply(this, args);
    };
}

/**
 *
 * @param {{round: Number, exceptions: EXCEPTIONS}} config
 */
function getDefinedObjects(config) {
    var DEFINED_OBJECTS = {
        Object: {
            __constructor__: function object__constructor__() {
                return {
                    value: null,
                    type: "Object",
                    methods: DEFINED_OBJECTS.Object['__methods__'],
                    parent: null
                };
            },
            __methods__: {
                __ge__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object_ge__method(toValue) {
                        if (this.value &gt;= toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                },
                __le__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object__le__method(toValue) {
                        if (this.value &lt;= toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                },

                __gt__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object__gt__method(toValue) {
                        if (this.value &gt; toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                },

                __lt__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object__lt__method(toValue) {
                        if (this.value &lt; toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                },

                __neq__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object__neq__method(toValue) {
                        if (this.value !== toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                },

                __eq__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function object__eq__method(toValue) {
                        if (this.value === toValue.value) {
                            return DEFINED_OBJECTS.Boolean.__constructor__(true);
                        }

                        return DEFINED_OBJECTS.Boolean.__constructor__(false);
                    })
                }
            }
        },

        Array: {
            __constructor__: function array__constructor__(count, values) {
                values = values || [];

                var value = [];
                var i = 0;

                for (i; i &lt; values.length; i += 1) {
                    value[i] = values[i];
                }

                for (; i &lt; count; i += 1) {
                    value.push(DEFINED_OBJECTS.Number.__constructor__(0));
                }

                return {
                    value: value,
                    type: "Array",
                    methods: DEFINED_OBJECTS.Array['__methods__'],
                    parent: DEFINED_OBJECTS.Object
                };
            },

            __methods__: {
                __get__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function array__get__code(index) {
                        if (index.type !== "Number") {
                            throw config.exceptions.CastErrorException(index.type, "Number");
                        }

                        if (this.value[index.value] === undefined) {
                            throw config.exceptions.IndexOutOfBoundsException(this.type, index.value, this.value.length);
                        }

                        return this.value[index.value];
                    })
                },
                __set__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function array__set__code(index, value) {
                        if (index.type !== "Number") {
                            throw config.exceptions.CastErrorException("String", "Number");
                        }

                        if (this.value[index.value] === undefined) {
                            throw config.exceptions.IndexOutOfBoundsException(this.type, index.value, this.value.length);
                        }

                        this.value[index.value] = value;

                        return this;
                    })
                }
            }
        },

        Boolean: {
            __constructor__: function boolean__constructor__(val) {
                return {
                    value: Boolean(val) || false,
                    type: "Boolean",
                    methods: DEFINED_OBJECTS.Boolean['__methods__'],
                    parent: DEFINED_OBJECTS.Object
                };
            },

            __methods__: {}
        },

        String: {
            __constructor__: function string__constructor__(val) {
                return {
                    value: String(val) || '',
                    type: "String",
                    methods: DEFINED_OBJECTS.String['__methods__'],
                    parent: DEFINED_OBJECTS.Object
                };
            },

            __methods__: {
                __add__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function string__add__method__(toValue) {
                        if (toValue.type === "Number" || toValue.type === "String") {
                            return DEFINED_OBJECTS.String.__constructor__(this.value + toValue.value);
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                }
            }
        },

        Number: {
            __constructor__: function number__constructor__(value) {
                return {
                    constructor: DEFINED_OBJECTS.Number['__constructor__'],
                    value: Number(value) || 0,
                    type: "Number",
                    methods: DEFINED_OBJECTS.Number['__methods__'],
                    parent: DEFINED_OBJECTS.Object
                };
            },
            __methods__: {
                __add__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__add__method(toValue) {
                        if (toValue.type === "Number") {
                            return DEFINED_OBJECTS.Number.__constructor__(this.value + toValue.value);
                        } else if (toValue.type === "String") {
                            return DEFINED_OBJECTS.String.__constructor__(this.value + toValue.value);
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },
                __sub__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__sub__method(toValue) {
                        if (toValue.type === "Number") {
                            return DEFINED_OBJECTS.Number.__constructor__(this.value - toValue.value);
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },

                __mul__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__mul__method(toValue) {
                        if (toValue.type === "Number") {
                            var value = this.value * toValue.value;
                            value = value.toFixed(config.round);

                            return DEFINED_OBJECTS.Number.__constructor__(parseFloat(value));
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },

                __div__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__div__method(toValue) {
                        if (toValue.type === "Number") {
                            var value = this.value / toValue.value;
                            value = value.toFixed(config.round);

                            return DEFINED_OBJECTS.Number.__constructor__(parseFloat(value));
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },
                __div_full__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__div_full__method(toValue) {
                        if (toValue.type === "Number") {
                            return DEFINED_OBJECTS.Number.__constructor__(~~(this.value / toValue.value));
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },
                __mod__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__mod__method(toValue) {
                        if (toValue.type === "Number") {
                            return DEFINED_OBJECTS.Number.__constructor__(this.value % toValue.value);
                        }

                        throw config.exceptions.CastErrorException(this.type, toValue.type);
                    })
                },
                __minus__: {
                    native: true,
                    jsCode: objectMocksMethodArgumentsDispatcherDecorator(function number__minus__method() {
                        return DEFINED_OBJECTS.Number.__constructor__(this.value * -1);
                    })
                }
            }
        }
    };

    return DEFINED_OBJECTS;
}

/***/ }),
/* 10 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i &lt; props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var EXCEPTIONS = exports.EXCEPTIONS = function () {
    function EXCEPTIONS(translations) {
        _classCallCheck(this, EXCEPTIONS);

        this.translations = {};

        this.translations = translations;
    }

    _createClass(EXCEPTIONS, [{
        key: "InstructionIsDefinedException",
        value: function InstructionIsDefinedException(instrName) {
            var defaultTranslation = "The instruction '{0}' has already been defined";
            this.name = "InstructionIsDefinedException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, instrName);

            return this;
        }
    }, {
        key: "CastErrorException",
        value: function CastErrorException(type, toType) {
            var defaultTranslation = "Cast exception '{0}' to type: '{1}'";
            this.name = "CastErrorException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, type, toType);
            return this;
        }
    }, {
        key: "IndexOutOfBoundsException",
        value: function IndexOutOfBoundsException(type, index, length) {
            var defaultTranslation = "Exception ({0}): index {1} is out of bounds";
            this.name = "IndexOutOfBoundsException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, type, index);

            return this;
        }
    }, {
        key: "ToFewArgumentsException",
        value: function ToFewArgumentsException(functionName, expected) {
            var defaultTranslation = "To few arguments for function '{0}' (expected at least: {1} arguments)";
            this.name = "ToFewArgumentsException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, functionName, expected);

            return this;
        }
    }, {
        key: "MethodNotFoundException",
        value: function MethodNotFoundException(instrName) {
            var defaultTranslation = "Undefined method '{0}'";
            this.name = "MethodNotFoundException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, instrName);

            return this;
        }
    }, {
        key: "UndefinedVariableNameException",
        value: function UndefinedVariableNameException(varName, functionName) {
            var defaultTranslation = "Usage of undefined variable '{0}' in function '{1}'";
            this.name = "UndefinedVariableNameException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, varName, functionName);

            return this;
        }
    }, {
        key: "UndefinedFunctionNameException",
        value: function UndefinedFunctionNameException(varName, functionName) {
            var defaultTranslation = "Usage of undefined function '{0}' in function '{1}'";
            this.name = "UndefinedFunctionNameException";
            this.message = window.StringUtils.format(this.translations[this.name] || defaultTranslation, varName, functionName);

            return this;
        }
    }]);

    return EXCEPTIONS;
}();

/***/ }),
/* 11 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.CODE_GENERATORS = undefined;

var _languageUtils = __webpack_require__(2);

function uidDecorator(fn) {
    return function () {
        CODE_GENERATORS.uid += 1;
        return fn.apply(this, arguments);
    };
}

function getuid() {
    CODE_GENERATORS.uid += 1;
    return CODE_GENERATORS.uid;
}

var CODE_GENERATORS = exports.CODE_GENERATORS = {
    uid: 0,

    case: uidDecorator(
    /**
     * @param  {Object[]} variableDef
     * @param  {{option:String[], code:Object[]}[]} options
     */
    function bnf_case(variableDef, options) {
        var i = void 0,
            exitLabel = CODE_GENERATORS.uid + "_case_end",
            execCode = [(0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.case++;', '')];

        execCode = execCode.concat(variableDef); //On stack is variable value

        for (i = 0; i &lt; options.length; i += 1) {
            options[i].option.forEach(function (option) {
                execCode = execCode.concat(option); //Now on stack we have option to compare
                execCode.push((0, _languageUtils.generateJumpInstruction)('stack[stack.length - 2].value === stack.pop().value', CODE_GENERATORS.uid + '_case_option_' + i));
            });
        }
        execCode.push((0, _languageUtils.generateJumpInstruction)('true', exitLabel));

        for (i = 0; i &lt; options.length; i += 1) {
            execCode.push((0, _languageUtils.generateExecuteObject)('', CODE_GENERATORS.uid + '_case_option_' + i));
            execCode = execCode.concat(options[i].code);
            execCode.push((0, _languageUtils.generateJumpInstruction)('true', exitLabel));
        }
        execCode.push((0, _languageUtils.generateExecuteObject)('', exitLabel));
        execCode.push((0, _languageUtils.generateExecuteObject)('stack.pop();', ''));
        return execCode;
    }),

    /**
     * Try to find method in object. If object doesn't contains method then check his parent.
     * @param object {Object}
     * @param methodName {String}
     * @returns {Function}
     */
    getMethodFromObject: function bnf_getMethodFromObject(machineManager, object, methodName) {
        var methods = object.methods;
        while (true) {
            if (methods.hasOwnProperty(methodName)) {
                return methods[methodName];
            }

            if (object.parent == null) {
                throw machineManager.exceptions.MethodNotFoundException(methodName);
            }

            object = object.parent;
            methods = object.__methods__;
        }
    },

    /**
     * Manager should be added to each program. If method is called then this manager will find correct function.
     *  Built in methods:
     *  -get method from object. Call this object method as js call function with passed object and stack and builtIn arguments.
     *  -get retVal value and add it to stack
     *
     *
     * Objects and inheritance in pseudocode (Concept):
     *  -Add to machine new instruction evaluateJumpLabelAndJump which will execute code in label and will jump to generated label.
     *  -Add new object to machineManager.objectMocks
     *  -Use it in object call manager, if getMethodFromObject(a,b).native is True, then execute original code, if false then use evaluateJumpLabelAndJump to getMethodFromObject(a,b).labelCode where will be code to jump.
     *  -Each class should be saved in precessor code, each method should contains own label to jump, for example MyClass.myMethod should contains MyClass.myMethod label for jump
     *  -Before jump, set scope for this class and jump to method.
     *
     */
    getObjectCallManager: function bnf_getObjectCallManager() {
        var execCode = [];

        execCode.push((0, _languageUtils.generateExecuteObject)('', '1_get_object_call_manager'));

        var code = "";
        code += "machineManager.bnf.builtInMethodCall(machineManager, stack, machineManager.objectForInstructions, machineManager.objectMocks, next, pause, retVal);;";

        execCode.push((0, _languageUtils.generateExecuteObject)(code, ''));

        execCode.push((0, _languageUtils.generateExecuteObject)("actualIndex = functionsCallPositionStack.pop() + 1;", ""));

        return execCode;
    },

    case_option: function bnf_case_option(option, code) {
        return [{
            option: option,
            code: code
        }];
    },

    array: function bnf_array(yy, arrayName, arraySize, startValues) {
        var execCode = [];

        var code = 'var buff1 = [];';

        startValues = startValues || [];

        startValues.forEach(function (el) {
            code += ';buff1.push(stack.pop());';
            execCode = execCode.concat(el);
        });

        code += 'stack.push(buff1.reverse());';

        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].defined.push(arrayName);
        code = code + 'actualScope.' + arrayName + '= machineManager.objectMocks.Array.__constructor__.call({},' + arraySize + ', stack.pop());';

        execCode.push((0, _languageUtils.generateExecuteObject)(code, ''));

        return execCode;
    },

    var: function bnf_var(yy, varName) {
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].defined.push(varName);
        return (0, _languageUtils.generateExecuteObject)('actualScope.' + varName + ' = machineManager.objectMocks.Number.__constructor__.call({}, 0);');
    },

    var_start_value: function var_start_value(yy, varName, operation) {
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].defined.push(varName);

        operation.push((0, _languageUtils.generateExecuteObject)('actualScope.' + varName + ' = stack.pop();'));

        return operation;
    },

    function_call: function bnf_function_call(yy, functionName, args) {
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].fn.push(functionName);

        return CODE_GENERATORS.dispatchFunction(yy, functionName, args || []);
    },

    method_call: function bnf_method_call(methodName, args, operations) {
        var execObjects = [];

        //Call args code in reverse order to save it on stack
        for (var i = args.length - 1; i &gt;= 0; i--) {
            execObjects = execObjects.concat(args[i]);
        }

        execObjects = execObjects.concat(operations);

        execObjects.push((0, _languageUtils.generateExecuteObject)("stack.push('" + methodName + "');", ''));
        execObjects.push((0, _languageUtils.generateExecuteObject)("stack.push(" + args.length + ");", ''));

        execObjects.push((0, _languageUtils.generateExecuteObject)("functionsCallPositionStack.push(actualIndex);", ""));
        execObjects.push((0, _languageUtils.generateJumpInstruction)('true', '1_get_object_call_manager'));
        execObjects.push((0, _languageUtils.generateExecuteObject)("stack.push(retVal.value);", ''));

        return execObjects;
    },

    array_get: function bnf_array_get(variableName, operations) {
        return CODE_GENERATORS.method_call("__get__", [operations], variableName);
    },

    function: function bnf_function(yy, functionName, functionArgs, sectionsBlock, codeBlock) {
        var sections = sectionsBlock || [];

        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].args = functionArgs;
        yy.functionNames.pop();

        return CODE_GENERATORS.generateFunctionStart(functionArgs, functionName).concat(sections).concat(codeBlock).concat(CODE_GENERATORS.generateFunctionEnd(functionName));
    },

    function_declaration: function bnf_function_declaration(yy, functionName) {
        yy.presenterContext.state.variablesAndFunctionsUsage[functionName] = { defined: [], args: [], vars: [], fn: [] };
        yy.actualFunctionName = functionName;
        yy.presenterContext.state.definedByUserFunctions.push(functionName);
        yy.functionNames.push(functionName);

        return functionName;
    },

    assign_value_1: function bnf_assign_value_1(yy, variableName, operations) {
        operations.push((0, _languageUtils.generateExecuteObject)('actualScope.' + variableName + ' = stack.pop();', ''));
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].vars.push(variableName);

        return operations;
    },

    assign_value_2: function bnf_assign_value_2(operations) {
        operations.push((0, _languageUtils.generateExecuteObject)('stack.pop()'));

        return operations;
    },

    assign_array_value: function bnf_assign_array_value(variableName, operations, value) {
        return CODE_GENERATORS.method_call("__set__", [operations, value], variableName);
    },

    program_name: function bnf_program_name(yy, programName) {
        yy.actualFunctionName = '1_main';
        yy.presenterContext.state.variablesAndFunctionsUsage['1_main'] = { defined: [], args: [], vars: [], fn: [] };
        return programName;
    },

    argument: function bnf_argument(yy, argName) {
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].vars.push(argName);

        return [(0, _languageUtils.generateExecuteObject)('stack.push(actualScope.' + argName + ');', '')];
    },

    for_argument: function bnf_for_argument(yy, argName) {
        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].vars.push(argName);

        return 'actualScope.' + argName + '.value';
    },

    for_value_header: uidDecorator(function bnf_for_value_header(yy, variableName, from, to, by, comparator) {
        var execElements = [];

        yy.presenterContext.state.variablesAndFunctionsUsage[yy.actualFunctionName].vars.push(variableName);

        yy.labelsStack.push(CODE_GENERATORS.uid + '_for');
        yy.labelsStack.push(CODE_GENERATORS.uid + '_for_end');

        execElements = execElements.concat(from);
        execElements.push((0, _languageUtils.generateExecuteObject)("actualScope." + variableName + ' = machineManager.objectMocks.Number.__constructor__(stack.pop().value - ' + by + ');'));
        execElements.push((0, _languageUtils.generateExecuteObject)('', CODE_GENERATORS.uid + '_for'));
        execElements.push((0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.for++', ''));
        execElements = execElements.concat(to);
        execElements.push((0, _languageUtils.generateJumpInstruction)('!((Boolean(actualScope.' + variableName + ' = machineManager.objectMocks.Number.__constructor__(actualScope.' + variableName + '.value + ' + by + ')) || true) &amp;&amp; actualScope.' + variableName + ".value " + comparator + " stack.pop().value )", CODE_GENERATORS.uid + '_for_end'));
        return execElements;
    }),

    for_exiter: function bnf_for_exiter(yy) {
        var execElements = [],
            exitLabel = yy.labelsStack.pop(),
            checkerLabel = yy.labelsStack.pop();

        execElements.push((0, _languageUtils.generateJumpInstruction)('true', checkerLabel));
        execElements.push((0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.for--', exitLabel));
        return execElements;
    },

    /**
     * @param  {Object[]} expression
     * @param  {Object[]} code
     */
    if_instruction: uidDecorator(function bnf_if_instruction(expression, code) {
        var executableCode = [(0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.if++;')],
            if_end = CODE_GENERATORS.uid + "_end_if";

        executableCode = executableCode.concat(expression);
        executableCode.push((0, _languageUtils.generateJumpInstruction)('!Boolean(stack.pop().value);', if_end));
        executableCode = executableCode.concat(code);
        executableCode.push((0, _languageUtils.generateExecuteObject)('', if_end));

        return executableCode;
    }),

    /**
     * @param  {Object[]} expression
     * @param  {Object[]} ifCode
     * @param  {Object[]} elseCode
     */
    if_else_instruction: uidDecorator(function bnf_if_else_instruction(expression, ifCode, elseCode) {
        var executableCode = [(0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.if++;')],
            else_start = CODE_GENERATORS.uid + "_else_if",
            if_end = CODE_GENERATORS.uid + "_end_if";

        executableCode = executableCode.concat(expression);
        executableCode.push((0, _languageUtils.generateJumpInstruction)('!Boolean(stack.pop().value);', else_start));
        executableCode = executableCode.concat(ifCode);
        executableCode.push((0, _languageUtils.generateJumpInstruction)('true', if_end));
        executableCode.push((0, _languageUtils.generateExecuteObject)('', else_start));
        executableCode = executableCode.concat(elseCode);
        executableCode.push((0, _languageUtils.generateExecuteObject)('', if_end));

        return executableCode;
    }),

    while_header: uidDecorator(function bnf_while_header(yy, expression) {
        yy.labelsStack.push(CODE_GENERATORS.uid + "_while");
        yy.labelsStack.push(CODE_GENERATORS.uid + "_while_end");

        var execElements = [];

        execElements.push((0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.while++', CODE_GENERATORS.uid + "_while"));
        execElements = execElements.concat(expression);
        execElements.push((0, _languageUtils.generateJumpInstruction)('!Boolean(stack.pop().value)', CODE_GENERATORS.uid + "_while_end"));

        return execElements;
    }),

    while_exiter: function bnf_while_exiter(yy) {
        var exitLabel = yy.labelsStack.pop(),
            startWhileLabel = yy.labelsStack.pop(),
            execElements = [];

        execElements.push((0, _languageUtils.generateJumpInstruction)('true', startWhileLabel));
        execElements.push((0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.while--', exitLabel));

        return execElements;
    },

    return_value: function bnf_return_value(yy, returnCode) {
        var actualFunctionName = yy.functionNames[yy.functionNames.length - 1],
            execCommands = returnCode;

        execCommands.push((0, _languageUtils.generateExecuteObject)("retVal = {value: stack.pop()};", ""));
        execCommands.push((0, _languageUtils.generateJumpInstruction)('true', "1_" + actualFunctionName));

        return execCommands;
    },

    do_while_header: uidDecorator(function bnf_do_while_header(yy) {
        var execElements = [],
            enterLabel = CODE_GENERATORS.uid + "_do_while_enter";

        execElements.push((0, _languageUtils.generateExecuteObject)('machineManager.objectForInstructions.calledInstructions.doWhile++;', enterLabel));
        yy.labelsStack.push(enterLabel);

        return execElements;
    }),

    do_while_exiter: function bnf_do_while_exiter(yy, expression) {
        var execElements = [],
            enterLabel = yy.labelsStack.pop();

        execElements = execElements.concat(expression);
        execElements.push((0, _languageUtils.generateJumpInstruction)('Boolean(stack.pop().value)', enterLabel));

        return execElements;
    },

    generateFunctionStart: function presenter_generateFunctionStart(argsList, functionName) {
        var execObjects = [],
            i = void 0,
            initialCommand = "";

        // Set start label
        execObjects.push((0, _languageUtils.generateExecuteObject)('', functionName));

        initialCommand += "eax = stack.pop();\n"; //Size of args array
        initialCommand += "ebx = Math.abs(eax - " + argsList.length + ");\n";

        initialCommand += "if (eax &lt; " + argsList.length + ") throw machineManager.exceptions.ToFewArgumentsException('" + functionName + "'," + argsList.length + ");\n";

        initialCommand += "stack.push(actualScope);\n"; //Save actualScope on stack
        initialCommand += "actualScope = {};\n"; //Reset scope to default

        // Add to actualScope variables passed in stack, but in stack is actualScope saved! (while function call)
        for (i = argsList.length - 1; i &gt;= 0; i -= 1) {
            initialCommand += "actualScope['" + argsList[Math.abs(i - (argsList.length - 1))] + "'] = stack[stack.length - (2 + " + i + " + ebx)];\n";
        }

        execObjects.push((0, _languageUtils.generateExecuteObject)(initialCommand, '')); //Call it as code

        return execObjects;
    },

    generateFunctionEnd: function presenter_generateFunctionEnd(functionName) {
        var execCommands = [],
            exitCommand = "";

        execCommands.push((0, _languageUtils.generateExecuteObject)('retVal.value = machineManager.objectMocks.Number.__constructor__(0);', '')); //If code goes there without return, then add to stack default value

        execCommands.push((0, _languageUtils.generateExecuteObject)('', '1_' + functionName)); //Here return will jump. Define as 1_&lt;function_name&gt;.

        exitCommand += "actualScope = {};"; // Clear scope
        exitCommand += "actualScope = stack.pop();"; //Get saved scope

        execCommands.push((0, _languageUtils.generateExecuteObject)(exitCommand, ''));

        execCommands.push((0, _languageUtils.generateExecuteObject)("actualIndex = functionsCallPositionStack.pop() + 1;", ""));

        return execCommands;
    },

    /**
     * @param stack {Object[]}
     * @param consoleObj {UserConsole}
     * @param objects {Object[]} List of objects from objectMocks
     * @param next {Function}
     * @param pause {Function}
     * @param retVal {{value: Object}}
     * @returns {*|void}
     */
    builtInMethodCall: function presenter_builtInMethodCall(machineManager, stack, consoleObj, objects, next, pause, retVal) {
        var argsCount = stack.pop();
        var methName = stack.pop();
        var obj = stack.pop();
        var args = [consoleObj, objects, next, pause, retVal];

        var method = CODE_GENERATORS.getMethodFromObject(machineManager, obj, methName).jsCode;

        for (var i = 0; i &lt; argsCount; i++) {
            args.push(stack.pop());
        }

        return method.apply(obj, args);
    },

    /**
     * @param  {Object} yy
     * @param  {Object[]} firstVal array with calculations first value
     * @param  {Object[]} secVal array with calculations second value
     * @param  {('__add__'|'__sub__'|'__div__'|'__mul__'|'__div_full__'|'__mod__'|'__ge__'|'__le__'|'__gt__'|'__lt__'|'__neq__'|'__eq__')} operationType
     * @return {Object[]}
     */
    generateOperationCode: function presenter_generateOperationCode(yy, firstVal, secVal, operationType) {
        var execObjects = firstVal.concat(secVal),
            code = "",
            exitCode = "",
            preCode = "";

        code += "ebx = stack.pop();";
        code += "eax = stack.pop();";
        code += "stack.push(ebx);";
        code += "stack.push(eax);";
        code += "stack.push('" + operationType + "');";
        code += "stack.push(1);";
        code += "functionsCallPositionStack.push(actualIndex);";

        exitCode += "stack.push(retVal.value);";

        execObjects.push((0, _languageUtils.generateExecuteObject)(code, ""));
        execObjects.push((0, _languageUtils.generateJumpInstruction)('true', '1_get_object_call_manager'));
        execObjects.push((0, _languageUtils.generateExecuteObject)(exitCode, ''));
        return execObjects;
    },

    /**
     *
     * @param yy
     * @param {Object[]} firstVal
     * @param {Object[]} secVal
     */
    generateOptimizedAndOperationCode: function presenter_generateOptimizedAndOperationCode(yy, firstVal, secVal) {
        var execObjects = firstVal;
        var code = "";
        var exitLabel = getuid() + "_optimized_and_exiter";

        code += "eax = stack.pop();";
        code += "if (!eax.value) stack.push(eax);";

        execObjects.push((0, _languageUtils.generateExecuteObject)(code, ''));
        execObjects.push((0, _languageUtils.generateJumpInstruction)('!eax.value', exitLabel));

        execObjects = execObjects.concat(secVal);

        execObjects.push((0, _languageUtils.generateExecuteObject)('', exitLabel));

        return execObjects;
    },

    /**
     *
     * @param yy
     * @param {Object[]} firstVal
     * @param {Object[]} secVal
     */
    generateOptimizedOrOperationCode: function presenter_generateOptimizedAndOperationCode(yy, firstVal, secVal) {
        var execObjects = firstVal;
        var code = "";
        var exitLabel = getuid() + "_optimized_or_exiter";

        code += "eax = stack.pop();";
        code += "if (eax.value) stack.push(eax);";

        execObjects.push((0, _languageUtils.generateExecuteObject)(code, ''));
        execObjects.push((0, _languageUtils.generateJumpInstruction)('eax.value', exitLabel));

        execObjects = execObjects.concat(secVal);

        execObjects.push((0, _languageUtils.generateExecuteObject)('', exitLabel));

        return execObjects;
    },

    generateMinusOperation: function presenter_generateMinusOperation(execObjects) {
        var retVal = [].concat(execObjects);
        var code = "";
        var exitCode = "";

        code += "stack.push('__minus__');";
        code += "stack.push(0);";
        code += "functionsCallPositionStack.push(actualIndex);";

        exitCode += "stack.push(retVal.value);";

        retVal.push((0, _languageUtils.generateExecuteObject)(code, ""));
        retVal.push((0, _languageUtils.generateJumpInstruction)('true', '1_get_object_call_manager'));
        retVal.push((0, _languageUtils.generateExecuteObject)(exitCode, ''));
        return retVal;
    },

    dispatchFunction: function presenter_dispatchFunction(yy, functionName, args) {
        var execCode = [],
            clearStackCode = '',
            i = void 0;

        for (i = 1; i &lt;= args.length; i += 1) {
            execCode = execCode.concat(args[i - 1]);
            clearStackCode += 'stack.pop();';
        }

        if (yy.presenterContext.configuration.functions.hasOwnProperty(functionName)) {
            execCode = execCode.concat(CODE_GENERATORS.dispatchForBuiltInFunctions(functionName, args));
        } else {
            execCode.push((0, _languageUtils.generateExecuteObject)("stack.push(" + args.length + ");", ''));
            execCode.push((0, _languageUtils.generateExecuteObject)("functionsCallPositionStack.push(actualIndex);", "")); //Push actual index of code, function before end will return to that index
            execCode = execCode.concat(CODE_GENERATORS.dispatchUserFunction(functionName));
        }

        execCode.push((0, _languageUtils.generateExecuteObject)(clearStackCode, ''));
        execCode.push((0, _languageUtils.generateExecuteObject)('stack.push(retVal.value);', ''));
        return execCode;
    },

    dispatchUserFunction: function presenter_dispatchUserFunction(functionName) {
        var execCode = [];

        execCode.push((0, _languageUtils.generateJumpInstruction)('true', functionName));

        return execCode;
    },

    /**
     * Dispatch for build in function (function declared in properties)
     * Returned value is executed in machine scope, so next, pause, retVal are locally variable for each machine (function machineManager.codeExecutor is scope for this code (eval))
     * @param  {String} functionName
     * @param  {Array[]} args contains how to resolve each argument
     */
    dispatchForBuiltInFunctions: function presenter_dispatchForBuiltInFunctions(functionName, args) {
        var parsedArgs = [],
            i = void 0,
            code = void 0,
            execCode = [];

        // That must be there, because, we don't know how many args receive built in function, so we send all args to this function
        for (i = 1; i &lt;= args.length; i += 1) {
            parsedArgs.unshift("stack[stack.length - " + i + "]");
        }

        if (parsedArgs.length &gt; 0) {
            code = "machineManager.configuration.functions." + functionName + ".call({}, machineManager.objectForInstructions, machineManager.objectMocks, next, pause, retVal," + parsedArgs.join(",") + ");";
        } else {
            code = "machineManager.configuration.functions." + functionName + ".call({}, machineManager.objectForInstructions, machineManager.objectMocks, next, pause, retVal);";
        }

        execCode.push((0, _languageUtils.generateExecuteObject)(code, '', true));

        return execCode;
    },

    number_value: function presenter_number_value(yy, yytext) {
        return [(0, _languageUtils.generateExecuteObject)('stack.push(machineManager.objectMocks.Number.__constructor__.call({}, Number(' + yytext + ')))', '')];
    },

    string_value: function string_value(yy, chars) {
        return [(0, _languageUtils.generateExecuteObject)('stack.push(machineManager.objectMocks.String.__constructor__.call({},"' + (chars || '') + '"))', '')];
    }
};

/***/ }),
/* 12 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.UserConsole = UserConsole;
var consoleClasses = {
    "LINES_CONTAINER": "pseudoConsole-console-container",
    "CURSOR": "pseudoConsole-console-cursor",
    "ACTIVE_CURSOR": "pseudoConsole-console-cursor-active",
    "RIGHT_ELEMENT": "pseudoConsole-console-right-element",
    "LEFT_ELEMENT": "pseudoConsole-console-left-element",
    "TEXT_AREA": "pseudoConsole-console-textarea"
};

/**
 *
 * @param {jQuery} $element
 * @param {{inputChecker: Function}}config
 * @constructor
 */
function UserConsole($element, config) {
    this.container = $("&lt;pre&gt;&lt;/pre&gt;");
    this.$textArea = $("&lt;textarea class='pseudoConsole-console-textarea'&gt;&lt;/textarea&gt;");
    this.linesContainer = $("&lt;div class='" + consoleClasses.LINES_CONTAINER + "'&gt;&lt;/div&gt;");
    this.$parentElement = $element;
    this.lines = [];
    this.activeLineIndex = -1;
    this.isReadMode = false; //Console is waiting for user input
    this.isDisabled = false;
    this.config = config;
    this.characterWidth = 0;

    $element.append(this.container);
    $element.append(this.$textArea);

    this.container.append(this.linesContainer);

    this.addNewLine(true);
}

UserConsole.prototype = {
    generateLine: function generateLine(className) {
        if (!className) {
            className = '';
        }

        var $htmlObject = $("&lt;span&gt;&lt;/span&gt;"),
            $left = $("&lt;span class='" + className + " " + consoleClasses.LEFT_ELEMENT + "'&gt;&lt;/span&gt;"),
            $right = $("&lt;span class='" + className + " " + consoleClasses.RIGHT_ELEMENT + "'&gt;&lt;/span&gt;"),
            $cursor = $("&lt;span class='" + consoleClasses.CURSOR + "'&gt;&lt;/span&gt;");

        $htmlObject.append($left);
        $htmlObject.append($cursor);
        $htmlObject.append($right);

        return {
            $htmlObject: $htmlObject,
            elements: {
                $left: $left,
                $cursor: $cursor,
                $right: $right
            }
        };
    },

    /**
     * @param  {Boolean} isActive Activate this line automatically
     * @param  {String} [className] set class for that line
     */
    addNewLine: function addNewLine(isActive, className) {
        if (!className) {
            className = '';
        }

        var line = this.generateLine(className);
        this.lines.push(line);
        this.linesContainer.append(line.$htmlObject);

        if (isActive) {
            this.selectLineAsActive(this.lines.length - 1);
        }

        this.$parentElement[0].scrollTop = this.$parentElement[0].scrollHeight;
    },

    selectLineAsActive: function selectLineAsActive(index) {
        var activeLine = null;
        if (this.activeLineIndex &gt; -1) {
            activeLine = this.getActiveLine();
            activeLine.elements.$left.text(activeLine.elements.$left.text() + activeLine.elements.$right.text());
            activeLine.elements.$right.text('');
            activeLine.elements.$cursor.html('');
            activeLine.elements.$cursor.removeClass(consoleClasses.ACTIVE_CURSOR);
        }

        this.activeLineIndex = index;
        activeLine = this.lines[index];
        activeLine.elements.$cursor.html('&amp;nbsp;');
        activeLine.elements.$cursor.addClass(consoleClasses.ACTIVE_CURSOR);
    },
    /**
     * @returns {{$htmlObject: jQuery, elements: {$left: jQuery, $right: jQuery, $cursor: jQuery}}}
     */
    getActiveLine: function getActiveLine() {
        return this.lines[this.activeLineIndex];
    },

    /**
     * @param  {String} text
     * @param  {String} className
     */
    Write: function Write(text, className) {
        if (this.isReadMode) {
            //Dont write to console if is in read mode.
            return;
        }

        text = String(text);

        this.addNewLine(true, className);

        var lines = text.split('\n'),
            line = void 0,
            activeLine = this.getActiveLine(),
            i = void 0;

        for (i = 0; i &lt; lines.length - 1; i += 1) {
            line = lines[i];
            activeLine.elements.$left.text(activeLine.elements.$left.text() + line);
            this.addNewLine(true, className);
            activeLine = this.getActiveLine();
            activeLine.elements.$left.text("\n");
        }

        activeLine = this.getActiveLine();
        line = lines[i];
        activeLine.elements.$left.text(activeLine.elements.$left.text() + line);
        this.scrollRight();
    },

    ReadLine: function ReadLine(callback) {
        if (this.isReadMode) {
            return;
        }

        this.isReadMode = true;
        var self = this;

        this.readLineFunction(function (data) {
            self.isReadMode = false;
            callback(data);
        });
    },

    readLineFunction: function readLineFunction(onExitCallback) {
        if (!this.isReadMode) {
            return;
        }

        this.addNewLine(true);

        var textAreaElement = this.$textArea,
            parentElement = this.$parentElement,
            self = this;

        $(parentElement).on('click', function () {
            textAreaElement.off();
            textAreaElement.focus();

            textAreaElement.on('input', function () {
                return self.onInputCallback();
            });

            textAreaElement.on('keydown', function (event) {
                return self.onKeyDownCallback(event, onExitCallback);
            });
        });

        $(parentElement).click();
    },

    onInputCallback: function onInputCallback() {
        if (this.isDisabled) {
            return;
        }

        var textAreaElement = this.$textArea,
            activeLine = this.getActiveLine(),
            data = textAreaElement.val(),
            leftText = activeLine.elements.$left.text(),
            rightText = activeLine.elements.$right.text();

        textAreaElement.val('');

        if (!this.config.inputChecker(data, leftText + data + rightText)) {
            return false;
        }

        if (data.length &gt; 0) {
            if (data[data.length - 1] !== '\n') {
                leftText = leftText + data;
            }
        }

        activeLine.elements.$left.text(leftText);
        activeLine.elements.$right.text(rightText);

        this.scrollRight();

        return false;
    },

    onKeyDownCallback: function onKeyDownCallback(event, onExitCallback) {
        if (this.isDisabled) {
            return;
        }

        var textAreaElement = this.$textArea,
            activeLine = this.getActiveLine(),
            keycode = event.which || event.keycode,
            leftText = activeLine.elements.$left.text(),
            rightText = activeLine.elements.$right.text(),
            parentElement = this.$parentElement;

        if (keycode === 39 || keycode === 37 || keycode === 8 || keycode === 13) {
            if (keycode === 39) {
                //Left arrow
                if (rightText.length &gt; 0) {
                    leftText += rightText[0];
                    rightText = rightText.substring(1);
                }
            } else if (keycode === 37) {
                //Right arrow
                if (leftText.length &gt; 0) {
                    rightText = leftText[leftText.length - 1] + rightText;
                    leftText = leftText.substring(0, leftText.length - 1);
                }
            } else if (keycode === 8) {
                //Backspace
                leftText = leftText.substring(0, leftText.length - 1);
            } else if (keycode === 13) {
                if ((leftText + rightText).length &gt; 0) {
                    $(parentElement).off();
                    textAreaElement.off();
                    onExitCallback(leftText + rightText);
                }
            }

            activeLine.elements.$left.text(leftText);
            activeLine.elements.$right.text(rightText);
            textAreaElement.val('');

            this.scrollRight();
            return false;
        }
    },

    ReadChar: function ReadChar(callback) {
        if (this.isReadMode) {
            return;
        }

        this.isReadMode = true;

        this.addNewLine(true);

        var activeLine = this.getActiveLine(),
            textAreaElement = this.$textArea,
            parentElement = this.$parentElement,
            data = void 0,
            leftText = void 0,
            self = this;

        $(parentElement).on('click', function () {
            textAreaElement.off();
            textAreaElement.focus();

            textAreaElement.on('input', function () {
                if (self.isDisabled) {
                    return;
                }

                leftText = activeLine.elements.$left.text();
                data = textAreaElement.val();

                if (!self.config.inputChecker(data, data)) {
                    return;
                }

                if (data[data.length - 1] !== "\n") {
                    $(parentElement).off();
                    textAreaElement.off();
                    activeLine.elements.$left.text(leftText + data[data.length - 1]); //Get only last char
                    self.isReadMode = false;
                    textAreaElement.val('');
                    callback(data[data.length - 1]);
                }

                self.scrollRight();
            });
        });

        $(parentElement).click();
    },

    Reset: function Reset() {
        var textAreaElement = this.$textArea,
            parentElement = this.$parentElement;

        parentElement.off();
        textAreaElement.off();

        this.isReadMode = false;

        this.linesContainer.find('span').remove();
        this.lines = [];

        this.activeLineIndex = -1;

        this.addNewLine(true);

        this.$textArea.val('');
    },

    destroy: function destroy() {
        this.Reset();
    },

    disable: function disable() {
        this.isDisabled = true;
    },

    enable: function enable() {
        this.isDisabled = false;
    },

    scrollRight: function scrollRight() {
        var actualLine = this.getActiveLine();
        var cursorLeftPosition = actualLine.elements.$cursor.position().left;
        var actualScroll = this.$parentElement.scrollLeft();
        var parentWidth = this.$parentElement.width();

        this.$parentElement.scrollLeft(actualScroll + cursorLeftPosition - parentWidth / 2);
    }
};

/***/ }),
/* 13 */
/***/ (function(module, exports, __webpack_require__) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.codeExecutor = codeExecutor;

var _languageUtils = __webpack_require__(2);

/**
 * @param  {Object} parsedData parsed code by jison
 * @param  {Boolean} getScore if function will be called to get score
 * @param  {Object} machineManager
 */
function codeExecutor(parsedData, getScore, machineManager) {
    var actualIndex = 0,
        code = parsedData.code,
        timeoutId = 0,
        isEnded = false,
        startTime = new Date().getTime() / 1000,
        actualScope = {},
        // There will be saved actual variables
    stack = [],
        // Stack contains saved scopes
    functionsCallPositionStack = [],
        //Stack which contains information about actual executed code position.
    retVal = { value: 0 },
        // value returned by function,
    eax = { value: 0 },
        // Helper register used in generated code (used for saving temporary data while executing code)
    ebx = { value: 0 },
        // Helper register used in generated code (used for saving temporary data while executing code)
    id = window.Helpers.uuidv4(); // Each machine contains own unique id which will be saved in presenter

    function getIndexByLabel(label) {
        var i = void 0;
        for (i = 0; i &lt; code.length; i += 1) {
            if (code[i] &amp;&amp; code[i].label === label) {
                return i;
            }
        }
    }

    /**
     *  Execute each line of code generated by JISON
     * @returns {Boolean} false - if code was executed, true if program is ended
     */
    function executeLine() {
        var actualEntry = code[actualIndex];
        if (actualEntry) {
            if (actualEntry.type === _languageUtils.TYPES.EXECUTE) {
                eval(actualEntry.code);
                actualIndex += 1;
            } else if (actualEntry.type === _languageUtils.TYPES.JUMP) {
                if (eval(actualEntry.code)) {
                    actualIndex = getIndexByLabel(actualEntry.toLabel);
                } else {
                    actualIndex += 1;
                }
            }
            return false;
        }
        return true;
    }

    function pause() {
        clearTimeout(timeoutId);
    }

    function next() {
        if (!isEnded) {
            timeoutId = setTimeout(executeAsync, 1);
        }
    }

    function executeAsync() {
        next();
        try {
            isEnded = executeLine();
            if (isEnded) {
                pause();
            }
        } catch (e) {
            if (!e.message) {
                machineManager.state.console.Write(e + "\n", 'program-error-output');
            } else {
                machineManager.state.console.Write(e.message + "\n", 'program-error-output');
            }
            killMachine();
        }
    }

    function killMachine() {
        pause();
        delete machineManager.killMachine[id];
        actualScope = null;
        stack = null;
        functionsCallPositionStack = null;
        eax = null;
        ebx = null;
        isEnded = true;
        return true;
    }

    function executeCodeSyncWithMaxTime() {
        var actualTime = void 0;
        while (true) {
            actualTime = new Date().getTime() / 1000;
            if (actualTime - startTime &gt; machineManager.configuration.answer.maxTimeForAnswer.parsedValue) {
                killMachine();
                return;
            }
            try {
                isEnded = executeLine();
                if (isEnded) {
                    killMachine();
                    return;
                }
            } catch (e) {
                killMachine();
                return;
            }
        }
    }

    machineManager.killMachine[id] = killMachine;

    eval(parsedData.sections);

    if (getScore) {
        executeCodeSyncWithMaxTime();
    } else {
        executeAsync();
    }
}

/***/ }),
/* 14 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.validateFunction = validateFunction;
exports.validateFunctions = validateFunctions;
exports.validateAliases = validateAliases;
exports.validateAnswer = validateAnswer;
exports.validateMethod = validateMethod;
exports.validateModel = validateModel;
/**
 * Wrap each function or method defined by user by this code. It will set default values for function and initialize console for call
 * Functions pause and next will stop or resume machine which actually executes this code.
 * @param {String} userCode
 * @returns {string}
 */
function wrapMethodOrFunctionWithBuiltInCode(userCode) {
    var code = "var builtIn = {\n";
    code += "   console: arguments[0].console,\n";
    code += "   data: arguments[0].data,";
    code += "   objects: arguments[1],\n";
    code += "   retVal: arguments[4]\n";
    code += "};";
    code += "builtIn.console.nextIns = arguments[2];\n";
    code += "builtIn.console.pauseIns = arguments[3];\n";
    code += "arguments = Array.prototype.slice.call(arguments, 5)\n";

    code += userCode;

    return code;
}

function generateValidationError(errorCode) {
    return {
        isValid: false,
        errorCode: errorCode
    };
}

function validateFunction(functionToValidate) {
    var validatedFunction = void 0;

    if (!/^[A-Za-z_][a-zA-Z0-9_]*$/g.exec(functionToValidate.name)) {
        return generateValidationError("FN01");
    }

    try {
        validatedFunction = new Function(wrapMethodOrFunctionWithBuiltInCode(functionToValidate.body));
    } catch (e) {
        return generateValidationError("JS02");
    }

    return {
        isValid: true,
        value: {
            name: functionToValidate.name,
            body: validatedFunction
        }
    };
}

function validateFunctions(functions) {
    var validatedFunctions = {},
        i = void 0,
        validatedFunction = void 0;

    for (i = 0; i &lt; functions.length; i += 1) {
        if (functions[i].name.trim().length === 0) {
            continue;
        }

        validatedFunction = validateFunction(functions[i]);
        if (!validatedFunction.isValid) {
            return validatedFunction;
        }

        if (validatedFunctions[validatedFunction.value.name]) {
            return generateValidationError("FN02");
        }

        validatedFunctions[validatedFunction.value.name] = validatedFunction.value.body;
    }

    return {
        isValid: true,
        value: validatedFunctions
    };
}

function validateAliases(aliases) {
    var definedAliases = {},
        aliasKey = void 0,
        aliasName = void 0,
        exists = {};

    for (aliasKey in aliases) {
        if (aliases.hasOwnProperty(aliasKey)) {
            if (aliases[aliasKey].name &amp;&amp; !ModelValidationUtils.isStringEmpty(aliases[aliasKey].name.trim())) {
                aliasName = aliases[aliasKey].name.trim();

                if (!/^[A-Za-z_][a-zA-Z0-9_]*$/g.exec(aliasName)) {
                    return generateValidationError("AN01");
                }

                definedAliases[aliasKey] = aliases[aliasKey].name.trim();
            }
        }
    }

    for (aliasKey in definedAliases) {
        if (definedAliases.hasOwnProperty(aliasKey)) {
            if (exists[definedAliases[aliasKey]]) {
                return generateValidationError("AN02");
            }

            exists[definedAliases[aliasKey]] = true;
        }
    }

    return {
        isValid: true,
        value: definedAliases
    };
}

function validateParameters(params) {
    var parameters = [],
        i = void 0;
    for (i = 0; i &lt; params.length; i += 1) {
        parameters.push(params[i].value);
    }

    return {
        isValid: true,
        value: parameters
    };
}

function validateAnswer(model) {
    var runUserCode = ModelValidationUtils.validateBoolean(model.runUserCode),
        answerCode = model.answerCode,
        maxTimeForAnswer = ModelValidationUtils.validateFloatInRange(model.maxTimeForAnswer, 10, 0),
        validatedParameters = void 0,
        answerCodeFunction = void 0;

    if (runUserCode &amp;&amp; (!maxTimeForAnswer.isValid || maxTimeForAnswer.parsedValue === 0)) {
        return generateValidationError("IP01");
    }

    validatedParameters = validateParameters(model.runParameters);

    try {
        answerCodeFunction = new Function(answerCode);
    } catch (e) {
        return generateValidationError("IP02");
    }

    return {
        isValid: true,
        runUserCode: runUserCode,
        answerCode: answerCodeFunction,
        maxTimeForAnswer: maxTimeForAnswer,
        parameters: validatedParameters.value
    };
}

function validateUniquenessAliasesNamesAndFunctions(aliases, functions) {
    var aliasKey = void 0;

    for (aliasKey in aliases) {
        if (aliases.hasOwnProperty(aliasKey)) {
            if (functions[aliases[aliasKey]]) {
                return generateValidationError("FN03");
            }
        }
    }

    return {
        isValid: true
    };
}

/**
 * @param {{objectName: (Array|Number|String), methodName: String, methodBody: String}} method
 */
function validateMethod(method) {
    var validatedMethod = {};

    try {
        validatedMethod = {
            objectName: method.objectName,
            methodName: method.methodName,
            function: new Function(wrapMethodOrFunctionWithBuiltInCode(method.methodBody))
        };
    } catch (e) {
        return generateValidationError("JS01");
    }

    return {
        isValid: true,
        method: validatedMethod
    };
}

/**
 * @param {{objectName: (Array|Number|String), methodName: String, methodBody: String}[]} methods
 */
function validateMethods(methods) {
    var validatedMethods = [];

    methods.forEach(function (method) {
        var validatedMethod = validateMethod(method);

        if (!validatedMethod.isValid) {
            return validatedMethod;
        }

        validatedMethods.push(validatedMethod.method);
    });

    return {
        isValid: true,
        methods: validatedMethods
    };
}

function validateRound(model) {
    var round = model['mathRound'];

    if (round.trim() === '') {
        return {
            isValid: true,
            value: 20
        };
    }

    var parsedRound = parseInt(round, 10);

    if (isNaN(parsedRound)) {
        return {
            isValid: false,
            errorCode: "ER01"
        };
    }

    if (parsedRound &lt; 1) {
        return {
            isValid: false,
            errorCode: "ER02"
        };
    }

    if (parsedRound &gt; 20) {
        return {
            isValid: false,
            errorCode: "ER03"
        };
    }

    return {
        isValid: true,
        value: parsedRound
    };
}

function validateConsoleAvailableInput(model) {
    var consoleAvailableInput = model.consoleAvailableInput;

    if (consoleAvailableInput === "") {
        consoleAvailableInput = "All";
    }

    return {
        isValid: true,
        value: consoleAvailableInput
    };
}

/**
 *
 * @param {{translation: String}} translation
 */
function validateExceptionTranslation(translation) {
    if (translation.translation.trim() === "") {
        return {
            isValid: true,
            value: null
        };
    }

    return {
        isValid: true,
        value: translation.translation.trim()
    };
}

function validateExceptionsTranslation(model) {
    var translations = model.exceptionsTranslation;
    var validatedTranslations = {};

    for (var exceptionName in translations) {
        if (translations.hasOwnProperty(exceptionName)) {
            var validatedTranslation = validateExceptionTranslation(translations[exceptionName]);
            if (!validatedTranslation.isValid) {
                return validatedTranslation;
            }

            validatedTranslations[exceptionName] = validatedTranslation.value;
        }
    }

    return {
        isValid: true,
        value: validatedTranslations
    };
}

function validateModel(model, aliases) {
    var validatedAliases = void 0,
        validatedFunctions = void 0,
        validatedAnswer = void 0,
        isUniqueInAliasesAndFunctions = void 0,
        validatedMethods = void 0;

    validatedAliases = validateAliases(model.default_aliases);
    if (!validatedAliases.isValid) {
        return validatedAliases;
    }

    validatedFunctions = validateFunctions(model.functionsList);
    if (!validatedFunctions.isValid) {
        return validatedFunctions;
    }

    if (validatedAliases.isValid &amp;&amp; validatedFunctions.isValid) {
        isUniqueInAliasesAndFunctions = validateUniquenessAliasesNamesAndFunctions(validatedAliases.value, validatedFunctions.value);
        if (!isUniqueInAliasesAndFunctions.isValid) {
            return isUniqueInAliasesAndFunctions;
        }
    }

    validatedAnswer = validateAnswer(model);
    if (!validatedAnswer.isValid) {
        return validatedAnswer;
    }

    validatedMethods = validateMethods(model.methodsList);
    if (!validatedMethods.isValid) {
        return validatedMethods;
    }

    var validatedRound = validateRound(model);
    if (!validatedRound.isValid) {
        return validatedRound;
    }

    var validatedAvailableConsoleInput = validateConsoleAvailableInput(model);
    if (!validatedAvailableConsoleInput.isValid) {
        return validatedAvailableConsoleInput;
    }

    var validatedExceptionsTranslation = validateExceptionsTranslation(model);
    if (!validatedExceptionsTranslation.isValid) {
        return validatedExceptionsTranslation;
    }

    return {
        isValid: true,
        addonID: model.ID,
        isActivity: !ModelValidationUtils.validateBoolean(model.isNotActivity),
        isVisibleByDefault: ModelValidationUtils.validateBoolean(model['Is Visible']),
        functions: validatedFunctions.value,
        aliases: $.extend(aliases, validatedAliases.value),
        answer: validatedAnswer,
        methods: validatedMethods.methods,
        round: validatedRound.value,
        availableConsoleInput: validatedAvailableConsoleInput.value,
        exceptionTranslations: validatedExceptionsTranslation.value
    };
}

/***/ }),
/* 15 */
/***/ (function(module, exports) {

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.isLetter = isLetter;
exports.isDigit = isDigit;
function inRange(value, min, max) {
    return value &lt;= max &amp; value &gt;= min;
}

/**
 * Check if string contains only letters
 * @param {String} c
 * @returns {boolean}
 */
function isLetter(c) {
    for (var i = 0; i &lt; c.length; i++) {
        if (!_isLetter(c.charCodeAt(i))) {
            return false;
        }
    }

    return true;
}

/**
 * Check if string contains only digits
 * @param {String} c
 * @returns {boolean}
 */
function isDigit(c) {
    for (var i = 0; i &lt; c.length; i++) {
        if (!_isDigit(c.charCodeAt(i))) {
            return false;
        }
    }

    return true;
}

function _isLetter(character) {
    return inRange(character, 65, 90) || inRange(character, 97, 122) || inRange(character, 192, 687) || inRange(character, 900, 1159) || // latin letters
    inRange(character, 1162, 1315) || inRange(character, 1329, 1366) || inRange(character, 1377, 1415) || // cyrillic letters
    inRange(character, 1425, 1536) || inRange(character, 1569, 1610) || // arabic letters
    inRange(character, 0x3400, 0x9FFF) || inRange(character, 0x0620, 0x063F) || inRange(character, 0x0641, 0x064A); //chinese and japanese letters
}

function _isDigit(d) {
    return inRange(d, 0x0030, 0x0039) //standard european digits
    || inRange(d, 0x0660, 0x0669) || inRange(d, 0x06F0, 0x06F9) // arabic digits
    || inRange(d, 0x1040, 0x108F) || inRange(d, 0x5344, 0x5345) // chinese and japanese digits
    || d === 0x3007 || d === 0x5341 || d === 0x4E00 || d === 0x4E8C || d === 0x4E09 || d === 0x56DB || d === 0x4E94 || d === 0x0516D || d === 0x4E03 || d === 0x516B || d === 0x4E5D || d === 0x5341 || d === 0x767E || d === 0x5343 || d === 0x4E07 || d === 0x842C || d === 0x5104 || d === 0x4EBF || d === 0x5146;
}

/***/ })
/******/ ]);
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Puzzle" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Columns" nameLabel="Puzzle_property_columns" type="string"/>
		<property name="Rows" nameLabel="Puzzle_property_rows" type="string"/>
		<property isDefault="true" name="Image" nameLabel="Puzzle_property_image" type="image"/>
		<property displayName="Is Not Activity" name="isNotActivity" nameLabel="Puzzle_property_is_not_activity" type="boolean"/>
	</model>
<css>.puzzle-container {
    border: 2px solid black;
    border-bottom-width: 1px;
    border-right-width: 1px;
    overflow: hidden;
    position: relative;
}

.mark {
    position: absolute;
    z-index: 0;
    width: 35px;
    height: 34px;
}

.mark.wrong {
    background-image: url('resources/puzzle-mark-wrong.png');
    z-index: 5;
}

.mark.correct {
    background-image: url('resources/puzzle-mark-correct.png');
    z-index: 5;
}

.puzzle {
    border: 1px solid black;
    border-top-width: 0;
    border-left-width: 0;
    z-index: 1;
    float: left;
    cursor: pointer;
    position: absolute;
}

.selected {
    border-color: orange;
    z-index: 3;
}

.hovered-over-by-other {
    opacity: 0.8;
}

.being-hovered {
    z-index: 3;
}
</css><view>&lt;div class="puzzle-container"&gt;
    &lt;div&gt;
        &lt;img alt="Image" src="" style="visibility: hidden" /&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;img width="100%" height="100%"/&gt;
</preview><presenter>function AddonPuzzle_create() {
    var presenter = function () {};

    /* Global variables */
    var board = []; // Array that will hold the 2-dimentional representation of the board.
    var indexBoard = []; // board storing marks
    var savedBoard = [];

    var animation = false;
    var clickNumber = 0; //Check if this is first or second click

    //Keep data from first click
    var PieceOld;
    var PiecePos;
    var PiecePos2;

    var DragStartPos;
    var DraggedPiece;
    var hoverClass = "being-hovered";
    var hoveredOverByOtherClass = "hovered-over-by-other";

    var puzzleWidth = 0;
    var puzzleOuterWidth = 0;
    var puzzleHeight = 0;
    var puzzleOuterHeight = 0;
    var leftOffset = 0;
    var topOffset = 0;

    var puzzle = null;

    var $container;
    var jImg;
    var mark;

    var playerController;
    var eventBus;

    presenter.initialState = null;
    presenter.previousScore = 0;
    presenter.previousErrors = 0;
    presenter.isPreview = false;

    presenter.isShowAnswersActive = false;
    presenter.isErrorMode = false;

    presenter.lockedDeferredCommandQueue = false;
    var deferredCommandQueue = window.DecoratorUtils.DeferredSyncQueue(isReadyToExecuteCommand);

    function isReadyToExecuteCommand() {
        return presenter.runEndedDeferred.isResolved() &amp;&amp; !presenter.lockedDeferredCommandQueue;
    }

    function getElementDimensions(element) {
        element = $(element);

        return {
            border: {
                top: parseInt(element.css('border-top-width'), 10),
                bottom: parseInt(element.css('border-bottom-width'), 10),
                left: parseInt(element.css('border-left-width'), 10),
                right: parseInt(element.css('border-right-width'), 10)
            },
            margin: {
                top: parseInt(element.css('margin-top'), 10),
                bottom: parseInt(element.css('margin-bottom'), 10),
                left: parseInt(element.css('margin-left'), 10),
                right: parseInt(element.css('margin-right'), 10)
            },
            padding: {
                top: parseInt(element.css('padding-top'), 10),
                bottom: parseInt(element.css('padding-bottom'), 10),
                left: parseInt(element.css('padding-left'), 10),
                right: parseInt(element.css('padding-right'), 10)
            }
        };
    }

    function calculateOuterDistance(elementDimensions) {
        var top = elementDimensions.border.top;
        top += elementDimensions.margin.top;
        top += elementDimensions.padding.top;

        var bottom = elementDimensions.border.bottom;
        bottom += elementDimensions.margin.bottom;
        bottom += elementDimensions.padding.bottom;

        var left = elementDimensions.border.left;
        left += elementDimensions.margin.left;
        left += elementDimensions.padding.left;

        var right = elementDimensions.border.right;
        right += elementDimensions.margin.right;
        right += elementDimensions.padding.right;

        return {
            vertical: top + bottom,
            horizontal: left + right,
            top: top,
            bottom: bottom,
            left: left,
            right: right,
            paddingLeft: elementDimensions.padding.left,
            paddingTop: elementDimensions.padding.top
        };
    }

    function getOuterDistances() {
        var containerDimensions = getElementDimensions($container);
        var containerDistances = calculateOuterDistance(containerDimensions);

        var puzzle = $(document.createElement("div"));
        puzzle.addClass('puzzle');
        $container.append(puzzle);
        var puzzleDimensions = getElementDimensions(puzzle);
        var puzzleDistances = calculateOuterDistance(puzzleDimensions);
        $(puzzle).remove();

        return {
            container: containerDistances,
            puzzle: puzzleDistances
        };
    }

    function getMarkDimensions() {
        var tempMark = $(document.createElement('div'));
        $(tempMark).addClass('mark').addClass('correct');
        $container.append(tempMark);

        var markWidth = $(tempMark).width();
        var markHeight = $(tempMark).height();

        $(tempMark).remove();

        return {
            width: markWidth,
            height: markHeight
        };
    }

    function addBorderClasses() {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            h, v;

        for (h = 0; h &lt; columns; h++) {
            $(board[0][h]).addClass('top');
            $(board[rows - 1][h]).addClass('bottom');
        }

        for (v = 0; v &lt; rows; v++) {
            $(board[v][0]).addClass('left');
            $(board[v][columns - 1]).addClass('right');
        }
    }

    function initPuzzleBoard() {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns;
        for (var row = 0; row &lt; rows; row++) {
            board[row] = [];
            indexBoard[row] = [];
            for (var col = 0; col &lt; columns; col++) {
                mark = $(document.createElement('div'));
                mark.addClass('mark');
                mark.attr("position", row + "-" + col);
                indexBoard[row][col] = mark;

                puzzle = $(document.createElement("div"));
                puzzle.addClass('puzzle');
                puzzle.attr("position", row + "-" + col);
                puzzle.attr("href", "javascript:void( 0 );").click(clickHandler);
                board[row][col] = puzzle;
            }
        }
    }

    function initPuzzle(width, height) {
        var outerDistances = getOuterDistances();
        var markDimensions = getMarkDimensions();
        var containerWidth = width - outerDistances.container.horizontal;
        var containerHeight = height - outerDistances.container.vertical;
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns;

        puzzleWidth = parseInt(containerWidth / columns - outerDistances.puzzle.horizontal, 10);
        puzzleOuterWidth = puzzleWidth + outerDistances.puzzle.horizontal;
        puzzleHeight = parseInt(containerHeight / rows - outerDistances.puzzle.vertical, 10);
        puzzleOuterHeight = puzzleHeight + outerDistances.puzzle.vertical;

        topOffset = outerDistances.container.paddingTop;
        leftOffset = outerDistances.container.paddingLeft;

        var markHorizontalOffset = (puzzleOuterWidth - markDimensions.width) / 2;
        var markVerticalOffset = (puzzleOuterHeight - markDimensions.height) / 2;

        for (var row = 0; row &lt; rows; row++) {
            for (var col = 0; col &lt; columns; col++) {
                mark = indexBoard[row][col];
                mark.css({
                    top: ((puzzleHeight * row + markVerticalOffset) + "px"),
                    left: ((puzzleWidth * col + markHorizontalOffset) + "px")
                });
                indexBoard[row][col] = mark;
                $container.append(mark);

                puzzle = board[row][col];
                puzzle.css({
                    backgroundImage: "url( '" + jImg.attr("src") + "' )",
                    backgroundSize: width + "px " + height + "px",
                    backgroundRepeat: "no-repeat",
                    backgroundPosition: (
                        (col * -puzzleWidth) + "px " +
                            (row * -puzzleHeight) + "px"
                        ),
                    top: ((puzzleOuterHeight * row + topOffset) + "px"),
                    left: ((puzzleOuterWidth * col + leftOffset) + "px"),
                    width: puzzleWidth + 'px',
                    height: puzzleHeight + 'px'
                });
                board[row][col] = puzzle;
                $container.append(puzzle);

                // first add it to DOM, then apply draggable, so that it won't add position: relative to element
                addDraggableDroppable(puzzle);
            }
        }

        $container.css({
            width: (puzzleOuterWidth * columns) + 'px',
            height: (puzzleOuterHeight * rows) + 'px'
        });

        addBorderClasses();
        presenter.shuffle();
    }

    function addDraggableDroppable(puzzle) {
        puzzle.draggable({
            delay: 150, // to give more time before drag starts, to prevent drags when clicking
            // don't use container or revert, as those options are bugged in subtle ways
            start: function(event,ui) {
                // clear state if it was clicked before
                clickNumber = 0;

                // remove class selected, so that when user clicks on piece, and then starts to drag, it won't
                if (PieceOld)
                    PieceOld.removeClass('selected');

                // this prevents clickHandler from being called, because jquery would call it in such situation:
                // user is dragging element, and lifts mouse button, and *pointer is still over element when drag stops*
                ui.helper.off("click");

                DraggedPiece = ui.helper;
                DraggedPiece.addClass( hoverClass );

                DragStartPos = presenter.getPiecePositionData(DraggedPiece);

                ui.position.left = 0;
                ui.position.top = 0;
            },
            drag : function(event, ui) {
                const scale = getScale();
                const changeLeft = ui.position.left - ui.originalPosition.left;  // find change in left
                const newLeft = ui.originalPosition.left + changeLeft / scale.X; // adjust new left by our zoomScale

                const changeTop = ui.position.top - ui.originalPosition.top;  // find change in top
                const newTop = ui.originalPosition.top + changeTop / scale.Y; // adjust new top by our zoomScale

                ui.position.left = newLeft;
                ui.position.top = newTop;
            },
            stop: function(event,ui) {

                if (DraggedPiece) {
                    // revert position
                    DraggedPiece.animate({
                        left: (DragStartPos.left + "px"),
                        top: (DragStartPos.top + "px")
                        }, 200,
                        function() { ui.helper.removeClass( hoverClass ); }
                    );
                    DraggedPiece = null;
                    DragStartPos = null;
                } else {
                    // it was dropped on other puzzle before, but we still need to clear hover
                    ui.helper.removeClass( hoverClass );
                }

                sendEvents(puzzle);

                // bringing it back here directly would make the click be called on puzzle (see comment in start), so we want
                // to do it in next browser update cycle
                setTimeout(
                    function() { ui.helper.attr("href", "javascript:void( 0 );").click(clickHandler); }
                    ,  0 );
            }
        });

        puzzle.droppable({
            activate: function (event, ui) {
                const scale = getScale();
                // add marker to currently dragged element to scale its proportions
                if (presenter.$view.find( "." + hoverClass ).length &gt; 0 &amp;&amp; scale.X !== 1 &amp;&amp; scale.Y !== 1) {
                    $.ui.ddmanager.current.useScaledProportions = true;
                }
            },
            drop: function (event, ui) {
                if (!DragStartPos)
                    return;

                var DraggedOnPiece = $(this);
                DraggedOnPiece.removeClass(hoveredOverByOtherClass);

                var DragEndPos = presenter.getPiecePositionData(DraggedOnPiece);

                board[DragEndPos.row][DragEndPos.col].animate({
                    left: ((puzzleOuterWidth * DragStartPos.col + leftOffset) + "px"),
                    top: ((puzzleOuterHeight * DragStartPos.row + topOffset) + "px")
                }, 200);

                board[DragStartPos.row][DragStartPos.col].animate({
                    left: ((puzzleOuterWidth * DragEndPos.col + leftOffset) + "px"),
                    top: ((puzzleOuterHeight * DragEndPos.row + topOffset) + "px")
                }, 200);

                var temp = board[DragStartPos.row][DragStartPos.col];
                board[DragStartPos.row][DragStartPos.col] = DraggedOnPiece;
                board[DragEndPos.row][DragEndPos.col] = temp;

                replaceBorderClasses(board[DragStartPos.row][DragStartPos.col], board[DragEndPos.row][DragEndPos.col]);

                DraggedPiece = null;
                DragStartPos = null;
            },
            over: function (event, ui) {
                $(this).addClass(hoveredOverByOtherClass);
            },
            // Triggered when an accepted draggable is dragged out of the droppable
            out: function (event, ui) {
                $(this).removeClass(hoveredOverByOtherClass);
            }
        });
    }

    function getScale() {
        if (playerController) {
            const scaleInformation = playerController.getScaleInformation();
            if (scaleInformation.baseScaleX !== 1.0 ||
                scaleInformation.baseScaleY !== 1.0 ||
                scaleInformation.scaleX !== 1.0 ||
                scaleInformation.scaleY !== 1.0
            ) {
                return {X: scaleInformation.scaleX, Y: scaleInformation.scaleY};
            }
        }

        const $content = $("#content");
        if ($content.size() &gt; 0) {
            const contentElem = $content[0];
            const scaleX = contentElem.getBoundingClientRect().width / contentElem.offsetWidth;
            const scaleY = contentElem.getBoundingClientRect().height / contentElem.offsetHeight;
            return {X: scaleX, Y: scaleY};
        } else {
            return {X: 1.0, Y: 1.0};
        }
    }

    presenter.getPiecePositionData = function(piece) {
        var Pos = {
            top: parseInt(piece.css("top")),
            left: parseInt(piece.css("left"))
        };
        Pos.row = Math.floor(((Pos.top - topOffset) / puzzleOuterHeight) + 0.5);
        Pos.col = Math.floor(((Pos.left - leftOffset) / puzzleOuterWidth) + 0.5);
        return Pos;
    };

    /**
     * Fisher-Yates Shuffle algorithm: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
     * Original algorithm is based on flatt, one-dimension array. For our purposes (working on two-dimension arrays)
     * firstly we have to flatten the structure.
     *
     * Additionally Knuth allows items to be shuffled multiple times - in our case each puzzle (array element) has to be
     * shuffled once, but the whole procedure should be repeated at least twice.
     */

    presenter.getShuffleSequence = function (array) {
        var flatArray = [],
            shuffleSequence = [],
            row, column, counter, index;

        for (row = 0; row &lt; array.length; row++) {
            for (column = 0; column &lt; array[row].length; column++) {
                flatArray.push({ row: row, column: column });
            }
        }

        counter = flatArray.length - 1;

        // While there are at least two elements in the array we generate next shuffle sequence. If array has only one
        // element we end the sequence (there is no sense in shuffling puzzle in place).
        while (counter &gt;= 2) {
            index = (Math.random() * counter) | 0;

            shuffleSequence.push({
                row: { from: flatArray[counter].row, to: flatArray[index].row },
                column: { from: flatArray[counter].column, to: flatArray[index].column }
            });

            flatArray.splice(index, 1);
            flatArray.splice(-1, 1);

            counter -= 2;
        }
        return shuffleSequence;
    };

    presenter.shuffle = function () {
        var i, iteration,
            shuffleSequence, shuffle,
            $firstPiece, $secondPiece;

        animation = false; // Shuffling should be without animation


        for (iteration = 0; iteration &lt; 3; iteration++) {
            shuffleSequence = presenter.getShuffleSequence(board);

            for (i = 0; i &lt; shuffleSequence.length; i++) {
                shuffle = shuffleSequence[i];

                $firstPiece = board[shuffle.row.from][shuffle.column.from];
                $firstPiece.trigger({
                    type: "click",
                    triggered: true
                });


                $secondPiece = board[shuffle.row.to][shuffle.column.to];
                $secondPiece.trigger({
                    type: "click",
                    triggered: true
                });
            }
        }

        animation = true;
    };

    function elementHasClasses(element) {
        element = $(element);

        return {
            top: element.hasClass('top'),
            bottom: element.hasClass('bottom'),
            left: element.hasClass('left'),
            right: element.hasClass('right')
        };
    }

    function removeBorderClasses(element) {
        $(element).removeClass('top').removeClass('bottom').removeClass('left').removeClass('right');
    }

    function applyBorderClasses(element, classes) {
        for (var className in classes) {
            if (classes[className]) {
                $(element).addClass(className);
            }
        }
    }

    function replaceBorderClasses(firstElement, secondElement) {
        var firstElementClasses = elementHasClasses(firstElement);
        var secondElementClasses = elementHasClasses(secondElement);

        removeBorderClasses(firstElement);
        removeBorderClasses(secondElement);

        applyBorderClasses(firstElement, secondElementClasses);
        applyBorderClasses(secondElement, firstElementClasses);
    }

    function isSamePiece(piece1, piece2) {
        var piece1ID = $(piece1).attr('position'),
            piece2ID = $(piece2).attr('position');

        return piece1ID == piece2ID;
    }

    function clickHandler(event) {
        if(presenter.isShowAnswersActive) {
            return;
        }
        event.stopPropagation();

        if (presenter.isErrorMode) return;

        var Piece = $(this);
        // Check to see if we are in the middle of an animation.
        if (clickNumber == 0) {
            clickNumber = 1;
            PieceOld = $(this);
            PieceOld.addClass('selected');
            PiecePos = presenter.getPiecePositionData(Piece);
        } else {
            swapPieces(Piece, event);
        }
    }

    function swapPieces(Piece,event) {

        clickNumber = 0;
        PiecePos2 = presenter.getPiecePositionData(Piece);
        PieceOld.removeClass('selected');

        if (isSamePiece(PieceOld, Piece)) return;
        if (!event.triggered) presenter.configuration.shouldCalcScore = true;

        board[PiecePos2.row][PiecePos2.col] = PieceOld;
        board[PiecePos.row][PiecePos.col] = Piece;

        if (animation) {
            //Animate change of places
            board[PiecePos.row][PiecePos.col].animate({
                left: ((puzzleOuterWidth * PiecePos.col + leftOffset) + "px"),
                top: ((puzzleOuterHeight * PiecePos.row + topOffset) + "px")
            }, 200);

            board[PiecePos2.row][PiecePos2.col].animate({
                left: ((puzzleOuterWidth * PiecePos2.col + leftOffset) + "px"),
                top: ((puzzleOuterHeight * PiecePos2.row + topOffset) + "px")
            }, 200);
        } else {
            board[PiecePos.row][PiecePos.col].css({
                left: ((puzzleOuterWidth * PiecePos.col + leftOffset) + "px"),
                top: ((puzzleOuterHeight * PiecePos.row + topOffset) + "px")
            });
            board[PiecePos2.row][PiecePos2.col].css({
                left: ((puzzleOuterWidth * PiecePos2.col + leftOffset) + "px"),
                top: ((puzzleOuterHeight * PiecePos2.row + topOffset) + "px")
            });
        }

        replaceBorderClasses(board[PiecePos.row][PiecePos.col], board[PiecePos2.row][PiecePos2.col]);

        if (!event.triggered) {
            sendEvents(Piece);
        }
    }

    function sendEvents(puzzle) {
        sendValueChangedEvent(puzzle);
        if (presenter.isAllOK()) {
            sendAllOKEvent();
        }
    }

    function sendValueChangedEvent(puzzle) {
        var data = {
            'source': presenter.configuration.addonID,
            'item': puzzle.attr("position"),
            'value': '',
            'score': ''
        };
        eventBus.sendEvent('ValueChanged', data);
    }

    function setNormalMode() {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            rowIndex, colIndex;

        for (rowIndex = 0; rowIndex &lt; rows; rowIndex++) {
            for (colIndex = 0; colIndex &lt; columns; colIndex++) {
                indexBoard[rowIndex][colIndex].removeClass('wrong').removeClass('correct');
            }
        }
        presenter.setDraggableState("enable");
        presenter.isErrorMode = false;
    }

    presenter.setDraggableState = function(state) {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns;
        for (var row = 0; row &lt; rows; row++) {
            for (var column = 0; column &lt; columns; column++) {
                var element = board[row][column];
                element.draggable( state );
            }
        }
    };

    presenter.reset = deferredCommandQueue.decorate(function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.configuration.shouldCalcScore = true;
        setNormalMode();
        presenter.shuffle();

        if (presenter.configuration.isVisibleByDefault) {
            presenter.show();
        } else {
            presenter.hide();
        }
    });

    presenter.setMarkVisibility = function (isVisible) {
        // it will be called by createPreview, in which case indexBoard won't be created
        if (presenter.isPreview || indexBoard.length === 0) {
            return;
        }

        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            rowIndex, colIndex;

        for (rowIndex = 0; rowIndex &lt; rows; rowIndex++) {
            for (colIndex = 0; colIndex &lt; columns; colIndex++) {
                indexBoard[rowIndex][colIndex].css("visibility", isVisible ? "visible" : "hidden");
            }
        }
    }

    presenter.prepareBoardFromSavedState = function (savedBoard) {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            index, rowIndex, colIndex,
            newBoard = [],
            puzzle, savedPuzzle;

        animation = false;

        for (rowIndex = 0; rowIndex &lt; rows; rowIndex++) {
            newBoard[rowIndex] = [];
        }

        for (rowIndex = 0; rowIndex &lt; rows; rowIndex++) {
            for (colIndex = 0; colIndex &lt; columns; colIndex++) {
                puzzle = board[rowIndex][colIndex];
                for (index = 0; index &lt; savedBoard.length; index++) {
                    if (puzzle.attr("position") == savedBoard[index].position) {
                        savedPuzzle = savedBoard[index];
                        newBoard[savedPuzzle.row][savedPuzzle.col] = puzzle;
                        newBoard[savedPuzzle.row][savedPuzzle.col].css({
                            left: ((puzzleOuterWidth * savedPuzzle.col + leftOffset) + "px"),
                            top: ((puzzleOuterHeight * savedPuzzle.row + topOffset) + "px")
                        });
                        savedBoard.splice(index, 1);
                    }
                }
            }
        }

        board = newBoard;
        animation = true;
    };

    presenter.saveBoard = function () {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            rowIndex, colIndex,
            tmpBoard = [];


        for (rowIndex = 0; rowIndex &lt; rows; rowIndex++) {
            for (colIndex = 0; colIndex &lt; columns; colIndex++) {
                var card = {};
                card.row = rowIndex;
                card.col = colIndex;
                card.position = board[rowIndex][colIndex].attr("position");
                tmpBoard.push(card);
            }
        }
        savedBoard = tmpBoard;
    };

    presenter.getState = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (!presenter.runEndedDeferred.isResolved() &amp;&amp; presenter.initialState !== null) {
            return presenter.initialState;
        }

        presenter.saveBoard();

        return JSON.stringify({
            visible: presenter.configuration.isVisible,
            board: savedBoard,
            shouldCalcScore: presenter.configuration.shouldCalcScore,
            score: presenter.getScore(),
            errors: presenter.getErrorCount()
        });
    };

    presenter.setState = function (state) {
        if (!state) return;
        if (!presenter.runEndedDeferred.isResolved()) {
            presenter.initialState = state;
        }

        presenter.lockedDeferredCommandQueue = true;

        const parsedState = JSON.parse(state);
        if (parsedState.score) {
            presenter.previousScore = parsedState.score;
        }

        if (parsedState.errors) {
            presenter.previousErrors = parsedState.errors;
        }

        presenter.runEnded.then(function () {
            presenter.lockedDeferredCommandQueue = false; // unlock command execution to execute methods like hide/show before resolving queue
            presenter.prepareBoardFromSavedState(parsedState.board);
            presenter.configuration.shouldCalcScore = parsedState.shouldCalcScore;
            if (!parsedState.visible) {
                presenter.hide();
            } else {
                presenter.show();
            }
            presenter.resolveDeferredCommandQueue();
        });
    };

    presenter.getMaxScore = function () {
        if (presenter.isPreview || presenter.configuration.isNotActivity) {
            return 0;
        }
        return 1;
    };

    presenter.getScore = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }
        if (!isReadyToExecuteCommand()) {
            return presenter.previousScore;
        }

        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            row, col;

        for (row = 0; row &lt; rows; row++) {
            for (col = 0; col &lt; columns; col++) {
                if (board[row][col].attr("position") != indexBoard[row][col].attr("position")) {
                    return 0;
                }
            }
        }

        return presenter.configuration.shouldCalcScore ? 1 : 0;
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }
        if (!isReadyToExecuteCommand()) {
            return presenter.previousErrors;
        }

        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            errors = 0;

        for (var row = 0; row &lt; rows; row++) {
            for (var col = 0; col &lt; columns; col++) {
                if (board[row][col].attr("position") != indexBoard[row][col].attr("position")) {
                    errors++;
                }
            }
        }

        return presenter.configuration.shouldCalcScore ? errors : 0;
    };

    presenter.setWorkMode = deferredCommandQueue.decorate(function () {
        setNormalMode();
    });

    presenter.setShowErrorsMode = deferredCommandQueue.decorate(function () {
        if (presenter.configuration.isNotActivity) {
            return;
        }
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.setDraggableState("disable");
        presenter.isErrorMode = true;
        presenter.configuration.shouldCalcScore = true;

        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns,
            row, col;

        for (row = 0; row &lt; rows; row++) {
            for (col = 0; col &lt; columns; col++) {
                var isEqual = board[row][col].attr("position") != indexBoard[row][col].attr("position");
                if (isEqual) { //wrong answer
                    indexBoard[row][col].addClass('wrong');
                } else {
                    indexBoard[row][col].addClass('correct');
                }
            }
        }
    });

    presenter.setPlayerController = function (controller) {
        playerController = controller;
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.run = function (view, model) {
        presenter.registerRunEndedDeferred();

        presenter.isPreview = false;
        const upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        presenter.$view = $(view);
        $container = $(presenter.$view.find('.puzzle-container:first')[0]);
        initPuzzleBoard();
        setImageElement(function () {
            initPuzzle(presenter.configuration.width, presenter.configuration.height);
            if (!presenter.configuration.isVisibleByDefault) {
                syncHide();
            }
            presenter.runEndedDeferred.resolve();
        });

        presenter.runEnded.then(function() {
            if (!presenter.lockedDeferredCommandQueue) {
                presenter.resolveDeferredCommandQueue();
            }
        })
    };

    presenter.registerRunEndedDeferred = function () {
        presenter.runEndedDeferred = $.Deferred();
        presenter.runEnded = presenter.runEndedDeferred.promise();
    };

    function setImageElement(onLoadCallback) {
        jImg = $container.find("img:first");
        jImg.attr('src', presenter.configuration.image);
        jImg.attr('height', presenter.configuration.height);
        jImg.attr('width', presenter.configuration.width);
        jImg.load(function () {
            onLoadCallback();
        });
    }

    presenter.validateModel = function (model) {
        const isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        const isNotActivity = ModelValidationUtils.validateBoolean(model['isNotActivity']);
        return {
            isValid: true,
            addonID: model.ID,
            width: model.Width,
            height: model.Height,
            image: model.Image,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            shouldCalcScore: false,
            columns: presenter.validatePuzzleDimension(model.Columns),
            rows: presenter.validatePuzzleDimension(model.Rows),
            isNotActivity: isNotActivity
        };
    };

    presenter.validatePuzzleDimension = function (dimension) {
        var validatedRange = ModelValidationUtils.validateIntegerInRange(dimension, 10, 1);

        return validatedRange.isValid ? validatedRange.value : 4;
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeIsNotActivity(model);
    };

    presenter.upgradeIsNotActivity = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['isNotActivity'] === undefined) {
            upgradedModel['isNotActivity'] = 'False';
        }

        return upgradedModel;
    };

    presenter.createPreview = function (view, model) {
        presenter.isPreview = true;
        var element = view.getElementsByTagName('img')[0];
        element.setAttribute('src', model.Image);

        presenter.$view = $(view);
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorMode) return;

        const commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isAllOK': presenter.isAllOK,
            'reset': presenter.reset
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = deferredCommandQueue.decorate(function () {
        presenter.configuration.shouldCalcScore = true;
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
        presenter.setMarkVisibility(true);
    });

    // hide is used to hide the addon with deferred queue to prevent state desync when called in the middle of other actions
    presenter.hide = deferredCommandQueue.decorate(function () {
        syncHide();
    });

    // syncHide is used to immediately hide the addon without waiting for deferred queue
    // function needed to hide addon immediately in initialization process before setState continues to avoid state desync
    // (e.g. to prevent situation when show from setState will be called before hide from initialization)
    function syncHide() {
        presenter.configuration.shouldCalcScore = true;
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
        presenter.setMarkVisibility(false);
    }

    presenter.isAllOK = function () {
        presenter.configuration.shouldCalcScore = true;
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    function sendAllOKEvent() {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    }

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    function showCorrect() {
        var rows = presenter.configuration.rows,
            columns = presenter.configuration.columns;

        for (var row = 0; row &lt; rows; row++) {
            for (var column = 0; column &lt; columns; column++) {
                var element = board[row][column],
                    position = element.attr("position"),
                    splittedPosition = position.split("-");

                element.css({
                    left: ((puzzleOuterWidth * splittedPosition[1] + leftOffset) + "px"),
                    top: ((puzzleOuterHeight * splittedPosition[0] + topOffset) + "px")
                });

                element.addClass("show-answers");
            }
        }
    }

    presenter.showAnswers = deferredCommandQueue.decorate(function () {
        if (presenter.configuration.isNotActivity) {
            return;
        }
        presenter.setWorkMode();
        presenter.saveBoard();
        presenter.isShowAnswersActive = true;
        showCorrect();
        presenter.setDraggableState("disable");
    });

    presenter.hideAnswers = deferredCommandQueue.decorate(function () {
        if (presenter.configuration.isNotActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.isShowAnswersActive = false;
        $container.find(".show-answers").removeClass("show-answers");
        presenter.prepareBoardFromSavedState(savedBoard);
        presenter.setDraggableState("enable");
    });

    presenter.onDestroy = function () {
        if (!presenter.runEndedDeferred.isResolved()) {
            presenter.runEndedDeferred.reject();
        }
        if (deferredCommandQueue.queue.length !== 0) {
            deferredCommandQueue.queue.length = 0;
        }
    };

    presenter.resolveDeferredCommandQueue = function() {
        deferredCommandQueue.resolve();
    };

    return presenter;
}

AddonPuzzle_create.__supported_player_options__ = {
    resetInterfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Quiz" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="Questions" nameLabel="Quiz_property_Questions" type="list">
            <property isLocalized="true" name="Question" nameLabel="Quiz_property_Question" type="html"/>
            <property name="CorrectAnswer" nameLabel="Quiz_property_CorrectAnswer" type="string"/>
            <property name="WrongAnswer1" nameLabel="Quiz_property_WrongAnswer1" type="string"/>
            <property name="WrongAnswer2" nameLabel="Quiz_property_WrongAnswer2" type="string"/>
            <property name="WrongAnswer3" nameLabel="Quiz_property_WrongAnswer3" type="string"/>
            <property isLocalized="true" name="Hint" nameLabel="Quiz_property_hint" type="html"/>
        </property>
        <property name="TestMode" nameLabel="Quiz_property_TestMode" type="boolean"/>
        <property name="ShowHelpButtons" nameLabel="Quiz_property_ShowHelpButtons" type="boolean"/>
        <property name="HelpButtonsMode" nameLabel="Quiz_property_HelpButtonsMode" type="{Both, Hint, 50/50}"/>
        <property name="NextAfterSelect" nameLabel="Quiz_property_NextAfterSelect" type="boolean"/>
        <property isLocalized="true" name="NextLabel" nameLabel="Quiz_property_NextLabel" type="string"/>
        <property isLocalized="true" name="GameLostMessage" nameLabel="Quiz_property_GameLostMessage" type="html"/>
        <property isLocalized="true" name="GameWonMessage" nameLabel="Quiz_property_GameWonMessage" type="html"/>
        <property name="ShowSummary" nameLabel="Quiz_property_Summary" type="boolean"/>
        <property isLocalized="true" name="GameSummaryMessage" nameLabel="Quiz_property_GameSummaryMessage" type="html"/>
        <property isLocalized="true" name="CorrectGameMessage" nameLabel="Quiz_property_CorrectGameMessage" type="html"/>
        <property isLocalized="true" name="WrongGameMessage" nameLabel="Quiz_property_WrongGameMessage" type="html"/>
        <property name="Center vertically" nameLabel="Quiz_property_center_vertically" type="boolean"/>
        <property name="isActivity" nameLabel="Quiz_property_isActivity" type="boolean"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Quiz_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Quiz_property_speech_texts" type="staticlist">
            <property name="Question" nameLabel="Quiz_speech_text_question" type="staticrow">
                <property name="Question" nameLabel="Quiz_speech_text_question" type="string"/>
            </property>
            <property name="Answer" nameLabel="Quiz_speech_text_answer" type="staticrow">
                <property name="Answer" nameLabel="Quiz_speech_text_answer" type="string"/>
            </property>
            <property name="FiftyFiftyButton" nameLabel="Quiz_speech_text_fifty_fifty_button" type="staticrow">
                <property name="FiftyFiftyButton" nameLabel="Quiz_speech_text_fifty_fifty_button" type="string"/>
            </property>
            <property name="FiftyFiftyButtonWhenNotEnoughAnswers" nameLabel="Quiz_speech_text_fifty_fifty_button_when_not_enough_answers" type="staticrow">
                <property name="FiftyFiftyButtonWhenNotEnoughAnswers" nameLabel="Quiz_speech_text_fifty_fifty_button_when_not_enough_answers" type="string"/>
            </property>
            <property name="HintButton" nameLabel="Quiz_speech_text_hint_button" type="staticrow">
                <property name="HintButton" nameLabel="Quiz_speech_text_hint_button" type="string"/>
            </property>
            <property name="CommentField" nameLabel="Quiz_speech_text_comment_field" type="staticrow">
                <property name="CommentField" nameLabel="Quiz_speech_text_comment_field" type="string"/>
            </property>
            <property name="Hint" nameLabel="Quiz_speech_text_hint" type="staticrow">
                <property name="Hint" nameLabel="Quiz_speech_text_hint" type="string"/>
            </property>
            <property name="Summary" nameLabel="Quiz_speech_text_summary" type="staticrow">
                <property name="Summary" nameLabel="Quiz_speech_text_summary" type="string"/>
            </property>
            <property name="Selected" nameLabel="Quiz_speech_text_selected" type="staticrow">
                <property name="Selected" nameLabel="Quiz_speech_text_selected" type="string"/>
            </property>
            <property name="Correct" nameLabel="Quiz_speech_text_correct" type="staticrow">
                <property name="Correct" nameLabel="Quiz_speech_text_correct" type="string"/>
            </property>
            <property name="Wrong" nameLabel="Quiz_speech_text_wrong" type="staticrow">
                <property name="Wrong" nameLabel="Quiz_speech_text_wrong" type="string"/>
            </property>
            <property name="Inactive" nameLabel="Quiz_speech_text_inactive" type="staticrow">
                <property name="Inactive" nameLabel="Quiz_speech_text_inactive" type="string"/>
            </property>
            <property name="OutOf" nameLabel="Quiz_speech_text_out_of" type="staticrow">
                <property name="OutOf" nameLabel="Quiz_speech_text_out_of" type="string"/>
            </property>
        </property>
    </model>
<css>.quiz-error-layer {
    color: #FF0000;
    background-color: #FFFFFF;
    width:100%;
    height: 100%;
}

.question-wrapper {
    width: 100%;
    height: 100%;
}

.question-wrapper .question-title {
    text-align: center;
    height: 33%;
}

.question-wrapper .hint-button {
    background: url("resources/hint.svg") #c0c0c0 no-repeat center;
    width: 60px;
    height: 60px;
    cursor: pointer;
    float: right;
}

.question-wrapper .fifty-fifty {
    background: url("resources/50-50.svg") #c0c0c0 no-repeat center;
    width: 60px;
    height: 60px;
    cursor: pointer;
    float: right;
}

.question-wrapper .next-question-button {
    width: 60px;
    height: 60px;
    cursor: default;
    float: right;
    opacity: 0.5;
}

.question-wrapper .next-question-button.active {
    cursor: pointer;
    opacity: 1;
}

.question-wrapper .question-tips {
    display: inline-block;
    width: 80%;
    float: left;
    height: 34%;
}

.question-wrapper .question-hint-buttons {
    display: block;
    width: 20%;
    float: right;
}

.question-wrapper .question-hint-buttons .used {
    opacity: 0.5;
    cursor: default;
}

.question-wrapper .removed {
    opacity: 0.5;
}

.question-wrapper .question-tips .question-tip {
    display: inline-block;
    cursor: pointer;
    width: 50%;
    height: 50%;
}

.headers-of-answers {
    display: inline-flex;
    font-weight: bold;
}

.answers {
    width: 85%;
    display: inline-flex;
    margin-left: 0.5em;
}

.center-vertically{
    position: relative;
    top: 50%;
    transform: translateY(-50%);
}

.question-wrapper .question-tips .question-tip.correct {
    background-color: greenyellow;
}

.question-wrapper .question-tips .question-tip.wrong {
    background-color: #A52A2A;
}

.question-wrapper .question-tips .question-tip.option {
    background-color: #fba600;
}

.question-wrapper .question-hint-wrapper {
    height: 33%;
    width: 80%;
    float: left;
    display: block;
}

.question-wrapper .question-hint {
    background-color: #FFFF00;
    color: #000000;
    width: 100%;
    height: 100%;
}

.question-wrapper .game-won-message-wrapper {
    background-color: #00aa00;
    color: #ffffff;
    width: 100%;
    height: 100%;
}
.question-wrapper .game-won-message {
    text-align: center;
}

.question-wrapper .game-lost-message-wrapper {
    background-color: #aa0000;
    color: #ffffff;
    width: 100%;
    height: 100%;
    position: relative;

}
.question-wrapper .game-lost-message {
    text-align: center;
}

.question-wrapper .game-summary-message-wrapper {
    background-color: #ffffff;
    color: #ffffff;
    width: 100%;
    height: 100%;
    color: #000000;
}
.question-wrapper .game-summary-message {
    text-align: center;
}
.question-wrapper.disabled,
.question-wrapper.disabled .hint-button,
.question-wrapper.disabled .fifty-fifty,
.question-wrapper.disabled .question-tips .question-tip {
    cursor: not-allowed;
}

.question-wrapper .question-tips .question-tip.correct-answer {
    background-color: #99FF55;
}

.question-wrapper .quiz-progress {
    width: 60px;
    float: right;
    text-align: center;
}
</css><view>&lt;div class="question-wrapper"&gt;&lt;/div&gt;
</view><preview>&lt;div class="question-wrapper"&gt;&lt;/div&gt;
</preview><presenter>function AddonQuiz_create() {
    /*
    *  KNOWN ISSUES:
    *
    *  The first time the addon is loaded, mathjax is rendered.
    *  Each reload content requires mathjax reload.
    *  When addon is loaded first time you shouldn't manually reload mathjax.
    *
    */

    var presenter = function () {
    };

    var playerController;
    var eventBus; // Modules communication
    var state;

    var ERRORS = {
        'QUESTION_REQUIRED': "At least 1 question is required.",
        'EMPTY_QUESTION': "At least one question is not specified",
        'MISSING_CORRECT_ANSWER': "At least one question doesn't have specified correct answer",
        'MISSING_WRONG_ANSWER': "At least one question doesn't have specified wrong answer",
        'MISSING_HINT': "At least one question doesn't have specified hint",
    };

    var CSS_CLASSES = {
        QUESTION_TITLE: 'question-title',
        QUESTION_TIP: 'question-tip',
        FIFTY_FIFTY: 'fifty-fifty',
        HINT_BUTTON: 'hint-button',
        NEXT_QUESTION_BUTTON: 'next-question-button',
        QUESTION_HINT_WRAPPER: 'question-hint-wrapper',
        QUESTION_HINT: 'question-hint',
        GAME_WON_MESSAGE: 'game-won-message',
        GAME_LOST_MESSAGE: 'game-lost-message',
        ACTIVE: 'active',
        CORRECT: 'correct',
        WRONG: 'wrong',
        REMOVED: 'removed',
        OPTION: 'option',
    };

    var DEFAULT_TTS_PHRASES = {
        Question: 'Question',
        Answer: 'Answer',
        FiftyFiftyButton: 'Fifty-Fifty button',
        FiftyFiftyButtonWhenNotEnoughAnswers: 'Fifty-Fifty button is inactive because there are less than 4 answers',
        HintButton: 'Hint button',
        CommentField: 'Comment field',
        Hint: 'Hint',
        Summary: 'Summary',
        Selected: 'Selected',
        Correct: 'Correct',
        Wrong: 'Wrong',
        Inactive: 'Inactive',
        OutOf: 'Out of',
    };

    function ConfigurationError(label) {
        return {
            name: 'ConfigurationError',
            message: ERRORS[label] || label
        }
    }

    presenter.activeElements = [];
    presenter.isLoaded = false;
    presenter.isWCAGOn = false;
    presenter.keyboardControllerObject = null;

    function setupDefaults() {
        state = {
            currentQuestion: 1,
            answersOrder: false,
            wasWrong: false,
            haveWon: false,
            fiftyFiftyUsed: false,
            hintUsed: null,
            selectedAnswer: null,
            isVisible: true,
            score: []
        };
        // addon's modes
        presenter.isErrorMode = false;
        presenter.isShowAnswersActive = false;
    }

    presenter.createAllOKEventData = function AddonQuiz_createAllOKEventData() {
        return {
            'source': presenter.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };
    };

    function validateQuestions(questions, helpButtons, helpButtonsMode) {
        if (questions.length &lt; 1) {
            throw ConfigurationError('QUESTION_REQUIRED');
        }
        for (var i = 0; i &lt; questions.length; i++) {
            var q = questions[i];
            if (ModelValidationUtils.isHtmlEmpty(q.Question)) {
                throw ConfigurationError('EMPTY_QUESTION');
            }
            if (ModelValidationUtils.isStringEmpty(q.CorrectAnswer)) {
                throw ConfigurationError('MISSING_CORRECT_ANSWER');
            }
            if (ModelValidationUtils.isStringEmpty(q.WrongAnswer1) &amp;&amp; ModelValidationUtils.isStringEmpty(q.WrongAnswer2) &amp;&amp; ModelValidationUtils.isStringEmpty(q.WrongAnswer3)
            ) {
                throw ConfigurationError('MISSING_WRONG_ANSWER');
            }
            if (helpButtons &amp;&amp; ModelValidationUtils.isHtmlEmpty(q.Hint) &amp;&amp; helpButtonsMode !== '50/50') {
                throw ConfigurationError('MISSING_HINT');
            }
        }
        return questions;
    }

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            Question: DEFAULT_TTS_PHRASES.Question,
            Answer: DEFAULT_TTS_PHRASES.Answer,
            FiftyFiftyButton: DEFAULT_TTS_PHRASES.FiftyFiftyButton,
            FiftyFiftyButtonWhenNotEnoughAnswers: DEFAULT_TTS_PHRASES.FiftyFiftyButtonWhenNotEnoughAnswers,
            HintButton: DEFAULT_TTS_PHRASES.HintButton,
            CommentField: DEFAULT_TTS_PHRASES.CommentField,
            Hint: DEFAULT_TTS_PHRASES.Hint,
            Summary: DEFAULT_TTS_PHRASES.Summary,
            Selected: DEFAULT_TTS_PHRASES.Selected,
            Correct: DEFAULT_TTS_PHRASES.Correct,
            Wrong: DEFAULT_TTS_PHRASES.Wrong,
            Inactive: DEFAULT_TTS_PHRASES.Inactive,
            OutOf: DEFAULT_TTS_PHRASES.OutOf,
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            Question: getSpeechTextProperty(
                speechTexts.Question.Question,
                presenter.speechTexts.Question),
            Answer: getSpeechTextProperty(
                speechTexts.Answer.Answer,
                presenter.speechTexts.Answer),
            FiftyFiftyButton: getSpeechTextProperty(
                speechTexts.FiftyFiftyButton.FiftyFiftyButton,
                presenter.speechTexts.FiftyFiftyButton),
            FiftyFiftyButtonWhenNotEnoughAnswers: getSpeechTextProperty(
                speechTexts.FiftyFiftyButtonWhenNotEnoughAnswers.FiftyFiftyButtonWhenNotEnoughAnswers,
                presenter.speechTexts.FiftyFiftyButtonWhenNotEnoughAnswers),
            HintButton: getSpeechTextProperty(
                speechTexts.HintButton.HintButton,
                presenter.speechTexts.HintButton),
            CommentField: getSpeechTextProperty(
                speechTexts.CommentField.CommentField,
                presenter.speechTexts.CommentField),
            Hint: getSpeechTextProperty(
                speechTexts.Hint.Hint,
                presenter.speechTexts.Hint),
            Summary: getSpeechTextProperty(
                speechTexts.Summary.Summary,
                presenter.speechTexts.Summary),
            Selected: getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.Selected),
            Correct: getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.Correct),
            Wrong: getSpeechTextProperty(
                speechTexts.Wrong.Wrong,
                presenter.speechTexts.Wrong),
            Inactive: getSpeechTextProperty(
                speechTexts.Inactive.Inactive,
                presenter.speechTexts.Inactive),
            OutOf: getSpeechTextProperty(
                speechTexts.OutOf.OutOf,
                presenter.speechTexts.OutOf)
        };
    };

    presenter.setupConfig = function AddonQuiz_setupConfig(model) {
        var helpButtons = ModelValidationUtils.validateBoolean(model['ShowHelpButtons']);
        presenter.setSpeechTexts(model['speechTexts']);
        presenter.config = {
            visibility: ModelValidationUtils.validateBoolean(model['Is Visible']),
            questions: validateQuestions(model['Questions'], helpButtons, model["HelpButtonsMode"]),
            helpButtons: helpButtons,
            nextLabel: model['NextLabel'] || '',
            gameLostMessage: model['GameLostMessage'],
            gameWonMessage: model['GameWonMessage'],
            gameSummaryMessage: model['GameSummaryMessage'],
            correctGameMessage: model['CorrectGameMessage'],
            wrongGameMessage: model['WrongGameMessage'],
            centerVertically: ModelValidationUtils.validateBoolean(model['Center vertically']),
            isActivity: ModelValidationUtils.validateBoolean(model['isActivity']),
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            nextAfterSelect: ModelValidationUtils.validateBoolean(model['NextAfterSelect']),
            testMode: ModelValidationUtils.validateBoolean(model['TestMode']),
            showSummary: ModelValidationUtils.validateBoolean(model['ShowSummary']),
            langTag: model["langAttribute"],
            helpButtonsMode: model["HelpButtonsMode"]
        }
    };

    function shuffle(a) {
        var j, x, i;
        for (i = a.length; i; i--) {
            j = Math.floor(Math.random() * i);
            x = a[i - 1];
            a[i - 1] = a[j];
            a[j] = x;
        }
    };

    function cleanWorkspace() {
        unbindEvents();
        presenter.activeElements = [];
        var wrapper = presenter.$view.find('.question-wrapper');
        wrapper.children().remove();
    };

    function showInHintArea($element) {
        presenter.hintWrapper.children().remove();
        presenter.hintWrapper.append($element);
    };

    function gameWonMessage() {
        var wrapper = $('&lt;div class="game-won-message-wrapper"&gt;&lt;/div&gt;');
        var message = $(`&lt;div class="${CSS_CLASSES.GAME_WON_MESSAGE}"&gt;&lt;/div&gt;`);
        if(presenter.config.showSummary) {
            message.html(window.xssUtils.sanitize(
                parseAltText(presenter.config.gameWonMessage) +
                '&lt;div&gt;' + parseAltText(presenter.config.gameSummaryMessage) +
                '&lt;div&gt;' + parseAltText(presenter.config.correctGameMessage) + ': ' + getScore() +
                '&lt;/div&gt;&lt;div&gt;' + parseAltText(presenter.config.wrongGameMessage) + ': ' + (presenter.config.questions.length - getScore()) +
                '&lt;/div&gt;' + '&lt;/div&gt;'));
        } else {
            message.html(window.xssUtils.sanitize(parseAltText(presenter.config.gameWonMessage)));
        }
        wrapper.append(message);
        showInHintArea(wrapper);
    };

    function gameLostMessage() {
        var wrapper = $('&lt;div class="game-lost-message-wrapper"&gt;&lt;/div&gt;');
        var message = $(`&lt;div class="${CSS_CLASSES.GAME_LOST_MESSAGE}"&gt;&lt;/div&gt;`);
        if(presenter.config.showSummary) {
            message.html(window.xssUtils.sanitize(
                parseAltText(presenter.config.gameLostMessage) +
                '&lt;div&gt;' + parseAltText(presenter.config.gameSummaryMessage) +
                '&lt;div&gt;' + parseAltText(presenter.config.correctGameMessage) + ': ' + getScore() +
                '&lt;/div&gt;&lt;div&gt;' + parseAltText(presenter.config.wrongGameMessage) + ': ' + (presenter.config.questions.length - getScore()) +
                '&lt;/div&gt;' + '&lt;/div&gt;'));
        }else{
            message.html(window.xssUtils.sanitize(parseAltText(presenter.config.gameLostMessage)));
        }
        wrapper.append(message);
        showInHintArea(wrapper);
    };

    function parseAltText(text) {
        return presenter.preview ? window.TTSUtils.parsePreviewAltText(text) : presenter.textParser.parse(text);
    }

    function getSelectItemAction(answer, $this) {
        var isCorrect = answer == getCurrentQuestion().CorrectAnswer;
        return function selectItemAction(e) {
            if (e) {
                e.stopPropagation();
                e.preventDefault();
            }
            var eventData = {
                'source': presenter.addonID,
                'item': state.currentQuestion,
                'value': '1',
                'score': '1'
            };
            state.selectedAnswer = answer;
            if (!presenter.config.testMode) {
                if (isCorrect) {
                    state.score[state.currentQuestion - 1] = 1;
                    $this.addClass(CSS_CLASSES.CORRECT);
                    eventBus.sendEvent('ValueChanged', eventData);
                    if (isLastQuestion()) {
                        gameWonMessage();
                        eventBus.sendEvent('ValueChanged', presenter.createAllOKEventData());
                        unbindEvents();
                    } else {
                        presenter.nextButton.addClass(CSS_CLASSES.ACTIVE);
                        unbindEvents(presenter.nextButton);
                        if (presenter.config.nextAfterSelect 
                            &amp;&amp; !presenter.isWCAGOn) {
                            unbindEvents();
                            setTimeout(function () {
                                nextAfterSelectCallback();
                            }, 500);
                        }
                    }
                } else {
                    state.score[state.currentQuestion - 1] = 0;
                    $this.addClass(CSS_CLASSES.WRONG);
                    eventData['score'] = '0';
                    eventBus.sendEvent('ValueChanged', eventData);
                    gameLostMessage();
                    state.wasWrong = true;
                    unbindEvents();
                }
            } else {
                $this.addClass(CSS_CLASSES.OPTION);

                if (isCorrect) {
                    state.score[state.currentQuestion - 1] = 1;
                } else {
                    state.score[state.currentQuestion - 1] = 0;
                    eventData['score'] = '0';
                }
                eventBus.sendEvent('ValueChanged', eventData);

                if (isLastQuestion()) {
                    if (getScore() &gt;= presenter.config.questions.length) {
                        gameWonMessage();
                        state.wasWrong = false;
                        eventBus.sendEvent('ValueChanged', presenter.createAllOKEventData());
                    }else{
                        gameLostMessage();
                        state.wasWrong = true;
                    }
                    unbindEvents();
                } else {
                    presenter.nextButton.addClass(CSS_CLASSES.ACTIVE);
                    unbindEvents(presenter.nextButton);
                    if (presenter.config.nextAfterSelect 
                        &amp;&amp; !presenter.isWCAGOn) {
                        unbindEvents();
                        setTimeout(function () {
                            nextAfterSelectCallback();
                        }, 500);
                    }
                }

            }
        }
    }

    function bindEvents() {
        var elements;
        if (arguments.length &gt; 0) {
            elements = Array.prototype.slice.call(arguments);
        } else {
            elements = presenter.activeElements;
        }
        unbindEvents();
        for (var i = 0; i &lt; elements.length; i++) {
            var $el = elements[i];
            $el.bind('click', $el.clickAction);
        }
    };

    function unbindEvents() {
        var args = Array.prototype.slice.call(arguments);
        for (var i = 0; i &lt; presenter.activeElements.length; i++) {
            var $el = presenter.activeElements[i];
            if (args.indexOf($el) &gt; -1) {
                continue;
            }
            $el.unbind('click', $el.clickAction);
        }
    };

    function getCurrentQuestion() {
        return presenter.config.questions[state.currentQuestion - 1];
    }

    function isLastQuestion() {
        return state.currentQuestion === presenter.config.questions.length;
    }

    function nextAfterSelectCallback() {
        nextButtonAction();
        bindEvents();
    }

    function fiftyFiftyAction(e) {
        if (e) {
            e.stopPropagation();
            e.preventDefault();
        }
        if (state.answersOrder.length &gt;= 4 &amp;&amp; !state.fiftyFiftyUsed) {
            // clue:
            state.fiftyFiftyUsed = true;
            unbindEvents();
            var removedItems = 0,
                i = -1;
            while (removedItems &lt; 2) {
                i++;
                if (i == state.answersOrder.length) {
                    i = 0;
                }
                var item = state.answersOrder[i];
                if (item === 0 || item == null) {
                    continue;
                }
                var x = Math.round(Math.random());
                if (x) {
                    removedItems++;
                    state.answersOrder[i] = null;
                }
            }
            presenter.showCurrentQuestion();
            bindEvents();
        }
    };

    function showHint() {
        var $hint = $(`&lt;div class="${CSS_CLASSES.QUESTION_HINT}"&gt;&lt;/div&gt;`);
        $hint.html(window.xssUtils.sanitize(parseAltText(getCurrentQuestion().Hint)));
        showInHintArea($hint);
        presenter.$view.find('.hint-button').addClass('used');
    }

    function nextButtonAction(e) {
        if (e) {
            e.stopPropagation();
            e.preventDefault();
        }
        if ((!state.wasWrong) &amp;&amp; (state.selectedAnswer !== null) &amp;&amp; state.currentQuestion &lt; presenter.config.questions.length) {
            state.selectedAnswer = null;
            state.answersOrder = false;
            state.currentQuestion++;
            presenter.showCurrentQuestion();
            if (presenter.keyboardControllerObject.keyboardNavigationActive) {
                presenter.keyboardControllerObject.markCurrentElement(0);
                presenter.keyboardControllerObject.readCurrentElement();
            }
            bindEvents();
        }
    }

    function hintAction(e) {
        if (e) {
            e.stopPropagation();
            e.preventDefault();
        }
        if (state.hintUsed === null) {
            state.hintUsed = state.currentQuestion;
            showHint();
        }
    };

    function addProgressBar(wrapper) {
        var progress = $('&lt;div class="quiz-progress"&gt;&lt;/div&gt;'),
            current = state.currentQuestion,
            len = presenter.config.questions.length,
            info = '&lt;span class="current-question-number"&gt;' + current + '&lt;/span&gt;' +
                '&lt;span class="divider"&gt;/&lt;/span&gt;' +
                '&lt;span class="questions-number"&gt;' + len + '&lt;/span&gt;';
        progress.html(info);
        wrapper.append(progress);
    };

    function showQuestion(q, showAnswer) {
        var $q = presenter.$view.find('.question-wrapper');
        var $title = $(`&lt;div class="${CSS_CLASSES.QUESTION_TITLE}"&gt;&lt;/div&gt;`);
        var $tips = $('&lt;div class="question-tips"&gt;&lt;/div&gt;');
        var $nextButton = $(`&lt;div class="${CSS_CLASSES.NEXT_QUESTION_BUTTON}"&gt;&lt;/div&gt;`);
        $nextButton.html(window.xssUtils.sanitize(parseAltText(presenter.config.nextLabel)));
        $nextButton.clickAction = nextButtonAction;

        cleanWorkspace();

        $title.html(window.xssUtils.sanitize(parseAltText(q.Question)));

        var tempAnswers = [q.CorrectAnswer];
        [q.WrongAnswer1, q.WrongAnswer2, q.WrongAnswer3].forEach(function (wrongAnswer) {
            if (wrongAnswer &amp;&amp; wrongAnswer.length &gt; 0) {
                tempAnswers.push(wrongAnswer);
            }
        });

        if (!state.answersOrder) {
            state.answersOrder = $.map(tempAnswers, function (element, index) {
                return index;
            });
            shuffle(state.answersOrder);
        }

        var answers = $.map(tempAnswers, function (element, index) {
            return index;
        });
        for (var i = 0; i &lt; answers.length; i++) {
            var index = state.answersOrder[i];
            if (index === null) {
                answers[i] = null;
            } else {
                answers[i] = tempAnswers[index];
            }
        }

        var labels = ['A: ', 'B: ', 'C: ', 'D: '];

        for (var i = 0; i &lt; answers.length; i++) {
            var $tip = $(`&lt;div class="${CSS_CLASSES.QUESTION_TIP}"&gt;&lt;/div&gt;`);
            var answer = answers[i];

            var headersOfAnswer = document.createElement('div');
            var $headersOfAnswer = $(headersOfAnswer);
            $headersOfAnswer.addClass("headers-of-answers");

            var divAnswers = document.createElement('div');
            var $divAnswers = $(divAnswers);
            $divAnswers.addClass('answers');

            var label = labels[i];
            $headersOfAnswer.text(label);
            $divAnswers.html(window.xssUtils.sanitize(parseAltText(answer || '')));
            if (answer === null) {
                $tip.addClass(CSS_CLASSES.REMOVED);
                $tip.clickAction = function () {
                };
            } else {
                $tip.clickAction = getSelectItemAction(answer, $tip);
            }

            if (answer == q.CorrectAnswer) {
                if (showAnswer) {
                    $tip.addClass('correct-answer');
                } else if (state.selectedAnswer == answer) {
                    $tip.addClass('correct-answer');
                    if (state.currentQuestion &lt; presenter.config.questions.length) {
                        $nextButton.addClass(CSS_CLASSES.ACTIVE);
                    }
                }
            } else if (state.wasWrong &amp;&amp; state.selectedAnswer == answer) {
                $tip.addClass(CSS_CLASSES.WRONG);
            }
            $tips.append($tip);
            $tip.append($headersOfAnswer);
            $tip.append($divAnswers);
            presenter.activeElements.push($tip)

            if (presenter.config.centerVertically) {
                $headersOfAnswer.addClass('center-vertically');
                $divAnswers.addClass('center-vertically');
            }
        }

        $q.append($title);
        $q.append($tips);
        var $buttons = $('&lt;div class="question-hint-buttons"&gt;&lt;/div&gt;');
        addProgressBar($buttons);
        $q.append($buttons);
        presenter.hintWrapper = $(`&lt;div class="${CSS_CLASSES.QUESTION_HINT_WRAPPER}"&gt;&lt;/div&gt;`);
        $q.append(presenter.hintWrapper);

        presenter.displayHint($q, $buttons);

        presenter.activeElements.push($nextButton);
        presenter.nextButton = $nextButton;
        if (!presenter.config.nextAfterSelect) {
            $buttons.append($nextButton);
        }
        if (state.wasWrong) {
            gameLostMessage();
        } else if (haveWon()) {
            gameWonMessage();
        } else if (state.selectedAnswer) {
            bindEvents(presenter.nextButton);
        } else if (!showAnswer) {
            bindEvents();
        }
        if (presenter.keyboardControllerObject) {
            presenter.reloadKeyboardController();
        }
    }

    presenter.displayHint = function ($q, $buttons) {
        if (presenter.config.helpButtons) {
            var $fiftyFifty = $(`&lt;div class="${CSS_CLASSES.FIFTY_FIFTY}"&gt;&lt;/div&gt;`);
            var $hintButton = $(`&lt;div class="${CSS_CLASSES.HINT_BUTTON}"&gt;&lt;/div&gt;`);
            $fiftyFifty.clickAction = fiftyFiftyAction;
            $hintButton.clickAction = hintAction;

            if (presenter.config.helpButtonsMode === 'Hint') {
                $buttons.append($hintButton);
                presenter.activeElements.push($hintButton);
            } else if (presenter.config.helpButtonsMode === '50/50') {
                $buttons.append($fiftyFifty);
                presenter.activeElements.push($fiftyFifty);
            } else {
                $buttons.append($fiftyFifty);
                $buttons.append($hintButton);
                presenter.activeElements.push($fiftyFifty);
                presenter.activeElements.push($hintButton);
            }

            $q.addClass('with-hint');
            if (state.fiftyFiftyUsed) {
                $fiftyFifty.addClass('used');
            }
            if (state.hintUsed) {
                $hintButton.addClass('used');
                if (state.hintUsed == state.currentQuestion) {
                    showHint();
                }
            }
        } else {
            $q.addClass('without-hint');
        }
    }

    function haveWon() {
        var q = getCurrentQuestion();
        return state.selectedAnswer == q.CorrectAnswer &amp;&amp; state.currentQuestion == presenter.config.questions.length;
    }

    function initializeLogic(view, model, preview) {
        setupDefaults();
        presenter.$view = $(view);
        presenter.preview = preview;
        try {
            presenter.setupConfig(model);
            presenter.showCurrentQuestion();
            presenter.config.questions.forEach(function () {
                state.score.push(0);
            });
        } catch (error) {
            var $error = $('&lt;div class="quiz-error-layer"&gt;&lt;/div&gt;');
            var text = "&lt;strong&gt;" + error.name + "&lt;/strong&gt;: " + error.message;
            $error.html(text);
            presenter.$view.find('.question-wrapper').append($error);
            presenter.config = {};
        }

        if (!preview) {
            bindEvents();
        }
        presenter.buildKeyboardController();
    };

    presenter.showCurrentQuestion = function AddonQuiz_showCurrentQuestion() {
        showQuestion(getCurrentQuestion(), false);
        renderMathJax();
    };

    function renderMathJax() {
        if (presenter.isLoaded) {
            reloadMathJax();
        }
    }

    function reloadMathJax() {
        const args = [];
        args.push("Typeset", MathJax.Hub, presenter.$view[0]);
        MathJax.Hub.Queue(args);
    }

    presenter.setPlayerController = function AddonQuiz_setPlayerController(controller) {
        playerController = controller;

        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setVisibility = function AddonQuiz_setVisibility(isVisible) {
        state.isVisible = isVisible;
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function AddonQuiz_hide() {
        presenter.setVisibility(false);
    };

    presenter.show = function AddonQuiz_show() {
        presenter.setVisibility(true);
    };

    presenter.run = function AddonQuiz_run(view, model) {
        eventBus = playerController.getEventBus();
        presenter.addonID = model.ID;
        presenter.view = view
        var upgradedModel = presenter.upgradeModel(model);
        initializeLogic(view, upgradedModel, false);
        presenter.setVisibility(presenter.config.isVisible);

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);

        MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        presenter.isLoaded = true;
    };

    presenter.createPreview = function AddonQuiz_createPreview(view, model) {

        var upgradedModel = presenter.upgradeModel(model);
        initializeLogic(view, upgradedModel, true);
    };

    presenter.getState = function AddonQuiz_getState() {
        if ("{}" === JSON.stringify(presenter.config)) {
            return "";
        }
        return JSON.stringify(state);
    };

    presenter.setState = function AddonQuiz_setState(gotState) {
        if (!gotState) {
            return;
        }
        state = JSON.parse(gotState);
        presenter.showCurrentQuestion();
        presenter.setVisibility(state.isVisible);
    };

    presenter.setShowErrorsMode = function AddonQuiz_setShowErrorsMode() {
        if (!presenter.config.isActivity || presenter.isErrorMode) {
            return;
        }

        presenter.isErrorMode = true;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        showErrorsMode();
    };

    presenter.setWorkMode = function AddonQuiz_setWorkMode() {
        if (!presenter.config.isActivity || !presenter.isErrorMode) {
            return;
        }

        presenter.isErrorMode = false;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        workMode();
    };

    function showErrorsMode() {
        presenter.disable();
    };

    function workMode() {
        presenter.enable();
    };

    presenter.reset = function AddonQuiz_reset() {
        presenter.setWorkMode();
        setupDefaults();
        presenter.showCurrentQuestion();
        bindEvents();
        presenter.setVisibility(presenter.config.isVisible);
    };

    presenter.getErrorCount = function AddonQuiz_getErrorCount() {
        if (!presenter.config.isActivity) return 0;
        return getErrorCount();
    };

    presenter.getMaxScore = function AddonQuiz_getMaxScore() {
        if (!presenter.config.isActivity) return 0;
        return getMaxScore();
    };

    presenter.getScore = function AddonQuiz_getScore() {
        if (!presenter.config.isActivity) return 0;
        return getScore();
    };

    function getErrorCount() {
        if (!presenter.config.testMode) {
            return state.wasWrong ? 1 : 0;
        } else {
            var errors = 0;
            state.score.forEach(function (e) {
                if (e == 0) {
                    errors = errors + 1;
                }
            });
            return errors;
        }
    }

    function getMaxScore() {
        return presenter.config.questions.length;
    }

    function getScore() {
        var score = 0;
        state.score.forEach(function (e) {
            if (e &gt; 0) {
                score = score + e;
            }
        });
        return score;
    }

    presenter.executeCommand = function AddonQuiz_executeCommand(name, params) {
        if (presenter.isErrorMode) {
            return;
        }

        var commands = {
            'isAllOK': presenter.isAllOK,
            'isAttempted': presenter.isAttempted,
            'show': presenter.show,
            'hide': presenter.hide,
            'disable': presenter.disable,
            'enable': presenter.enable,
            'reset': presenter.reset
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.isAllOK = function AddonQuiz_isAllOK() {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.isAttempted = function AddonQuiz_isAttempted() {
        return (state.currentQuestion &gt; 1) || state.wasWrong || (state.selectedAnswer !== null);
    };

    presenter.disable = function AddonQuiz_disable() {
        presenter.$view.find('.question-wrapper').addClass('disabled');
        unbindEvents();
    };

    presenter.enable = function AddonQuiz_enable() {
        presenter.$view.find('.question-wrapper').removeClass('disabled');
        bindEvents();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) { return; }

        unbindEvents();
        presenter.$view.off();
        presenter.eventBus = null;
        presenter.view = null;
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeAcceptWrongAnswers(model);
        upgradedModel = presenter.addLangTag(upgradedModel);
        upgradedModel = presenter.addSpeechTexts(upgradedModel);
        upgradedModel = presenter.addHelpButtonsMode(upgradedModel);

        return upgradedModel;
    };

    presenter.upgradeAcceptWrongAnswers = function (model) {
        if (!model['GameSummaryMessage']) {
            model['GameSummaryMessage'] = 'Score';
        }
        if (!model['CorrectGameMessage']) {
            model['CorrectGameMessage'] = 'Correct';
        }
        if (!model['WrongGameMessage']) {
            model['WrongGameMessage'] = 'Wrong';
        }
        return model;
    };

    presenter.addLangTag = function AddonTable_upgradeLangTag(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] =  '';
        }

        return upgradedModel;
    };

    presenter.addHelpButtonsMode = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('HelpButtonsMode')) {
            upgradedModel['HelpButtonsMode'] =  'Both';
        }

        return upgradedModel;
    }

    presenter.addSpeechTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);
        if (!model['speechTexts']) {
            upgradedModel['speechTexts'] = {
                Question: {Question: ''},
                Answer: {Answer: ''},
                FiftyFiftyButton: {FiftyFiftyButton: ''},
                FiftyFiftyButtonWhenNotEnoughAnswers: {FiftyFiftyButtonWhenNotEnoughAnswers: ''},
                HintButton: {HintButton: ''},
                CommentField: {CommentField: ''},
                Hint: {Hint: ''},
                Summary: {Summary: ''},
                Selected: {Selected: ''},
                Correct: {Correct: ''},
                Wrong: {Wrong: ''},
                Inactive: {Inactive: ''},
                OutOf: {OutOf: ''},
            }
        }
        return upgradedModel;
   };


    presenter.onEventReceived = function AddonQuiz_onEventReceived(eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function AddonQuiz_showAnswers() {
        if (!presenter.config.isActivity) {
            return;
        }
        presenter.isShowAnswersActive = true;
        showAnswers();
    };

    presenter.hideAnswers = function AddonQuiz_hideAnswers() {
        if (!presenter.config.isActivity || !presenter.isShowAnswersActive) {
            return;
        }
        presenter.isShowAnswersActive = false;
        hideAnswers();
    };

    function showAnswers() {
        showQuestion(getCurrentQuestion(), true);
        reloadMathJax();
    }

    function hideAnswers() {
        presenter.showCurrentQuestion();
    }

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function AddonQuiz_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.speakWithCallback = function (data, callbackFunction) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speakWithCallback(data, callbackFunction);
        }
    }

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new QuizKeyboardController(
                presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.reloadKeyboardController = function () {
        presenter.keyboardControllerObject.reload(
            presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find(`
            .${CSS_CLASSES.QUESTION_TITLE},
            .${CSS_CLASSES.QUESTION_TIP},
            .${CSS_CLASSES.FIFTY_FIFTY},
            .${CSS_CLASSES.HINT_BUTTON},
            .${CSS_CLASSES.NEXT_QUESTION_BUTTON},
            .${CSS_CLASSES.QUESTION_HINT_WRAPPER}
        `);
    };

    presenter.getQuestionTipsElementsForKeyboardNavigation = function () {
        return presenter.$view.find(`.${CSS_CLASSES.QUESTION_TIP}`);
    };

    presenter.getCommendFieldElementForKeyboardNavigation = function () {
        return presenter.$view.find(`.${CSS_CLASSES.QUESTION_HINT_WRAPPER}`);
    };

    presenter.getGameMessageElementForKeyboardNavigationFromElement = function ($element) {
        return $element.find(`.${CSS_CLASSES.GAME_LOST_MESSAGE}, .${CSS_CLASSES.GAME_WON_MESSAGE}`);
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    function QuizKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    QuizKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    QuizKeyboardController.prototype.constructor = QuizKeyboardController;

    QuizKeyboardController.prototype.exitWCAGMode = function () {
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    QuizKeyboardController.prototype.reload = function (elements, columnsCount) {
        this.isSelectEnabled = true;
        this.keyboardNavigationElements = elements;
        this.columnsCount = columnsCount;
        this.keyboardNavigationElementsLen = elements.length;
        if (this.keyboardNavigationActive) {
            this.refreshMarkOnCurrentElement();
            this.enter();
        }
    };

    QuizKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        this.readCurrentElement();
    };

    QuizKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    QuizKeyboardController.prototype.readCurrentElement = function () {
        this.readElement(this.keyboardNavigationCurrentElement);
    };

    QuizKeyboardController.prototype.refreshMarkOnCurrentElement = function () {
        this.markCurrentElement(this.keyboardNavigationCurrentElementIndex);
    };

    QuizKeyboardController.prototype.readElement = function (element) {
        var $element = this.getTarget(element, false);

        if ($element.hasClass(CSS_CLASSES.QUESTION_TITLE))
            presenter.speak(getQuestionTitleTextVoiceObject($element))
        else if ($element.hasClass(CSS_CLASSES.QUESTION_TIP))
            presenter.speak(getQuestionTipTextVoiceObject($element))
        else if ($element.hasClass(CSS_CLASSES.FIFTY_FIFTY))
            presenter.speak(getFiftyFiftyButtonTextVoiceObject())
        else if ($element.hasClass(CSS_CLASSES.HINT_BUTTON))
            presenter.speak(getHintButtonTextVoiceObject())
        else if ($element.hasClass(CSS_CLASSES.NEXT_QUESTION_BUTTON))
            presenter.speak(getNextQuestionTextVoiceObject($element))
        else if ($element.hasClass(CSS_CLASSES.QUESTION_HINT_WRAPPER))
            presenter.speak(getCommentFieldTextVoiceObject($element))
    };

    function getQuestionTitleTextVoiceObject($element) {
        var voicesArray = [];

        const prefix = presenter.speechTexts.Question;

        var language = document.documentElement.lang;

        const currentQuestionAsText = window.TTSUtils.numberToOrdinalNumber(state.currentQuestion, language, window.TTSUtils.GENDER.NEUTER);
        const questionsLengthAsText = window.TTSUtils.numberToOrdinalNumber(presenter.config.questions.length, language, window.TTSUtils.GENDER.FEMININE);
        const questionInfo = `${currentQuestionAsText} ${presenter.speechTexts.OutOf} ${questionsLengthAsText}`;
        const texts = [prefix, questionInfo, ];
        pushMessagesToTextVoiceObject(voicesArray, texts);
        
        return createAndConcatElementWithTextVoiceObject(voicesArray, $element);
    }

    function getQuestionTipTextVoiceObject($element) {
        var voicesArray = [];

        const prefix = presenter.speechTexts.Answer;
        pushMessageToTextVoiceObject(voicesArray, prefix);


        voicesArray = createAndConcatElementsWithTextVoiceObject(voicesArray, $element.children());
        if ($element.hasClass(CSS_CLASSES.REMOVED))
            return updateQuestionTipTextVoiceObjectWithRemoved(voicesArray);
        else if (isQuestionTipSelected($element))
            return updateQuestionTipTextVoiceObjectWithSelected(voicesArray, $element);
        return voicesArray;
    }

    function isQuestionTipSelected($questionTip) {
        return ($questionTip.hasClass(CSS_CLASSES.CORRECT)
                || $questionTip.hasClass(CSS_CLASSES.WRONG)
                || $questionTip.hasClass(CSS_CLASSES.OPTION));
    }

    function updateQuestionTipTextVoiceObjectWithRemoved(textVoiceObject) {
        var voicesArray = [];
        const status = presenter.speechTexts.Inactive;
        pushMessageToTextVoiceObject(voicesArray, status);
        return textVoiceObject.concat(voicesArray);
    }

    function updateQuestionTipTextVoiceObjectWithSelected(textVoiceObject, $element) {
        var voicesArray = [];
        var texts = [presenter.speechTexts.Selected, ]
        if ($element.hasClass(CSS_CLASSES.CORRECT)) 
            texts.push(presenter.speechTexts.Correct);
        else if ($element.hasClass(CSS_CLASSES.WRONG))
            texts.push(presenter.speechTexts.Wrong);
        pushMessagesToTextVoiceObject(voicesArray, texts);
        return textVoiceObject.concat(voicesArray);
    }

    function getFiftyFiftyButtonTextVoiceObject() {
        var voicesArray = [];
        var texts = [presenter.speechTexts.FiftyFiftyButton, ];

        var isNotEnoughAnswers = isLessThenFourAnswers();

        if (state.fiftyFiftyUsed || isNotEnoughAnswers || isQuestionAnswered()) {
            texts.push(presenter.speechTexts.Inactive);
            
            if (isNotEnoughAnswers) {
                texts.push(presenter.speechTexts.FiftyFiftyButtonWhenNotEnoughAnswers);
            }
        }
        pushMessagesToTextVoiceObject(voicesArray, texts);
        return voicesArray;
    }

    function isLessThenFourAnswers() {
        return state.answersOrder &amp;&amp; state.answersOrder.length &lt; 4;
    }

    function getHintButtonTextVoiceObject() {
        var voicesArray = [];
        var texts = [presenter.speechTexts.HintButton, ];
        if (state.hintUsed || isQuestionAnswered()) {
            texts.push(presenter.speechTexts.Inactive);
        }
        pushMessagesToTextVoiceObject(voicesArray, texts);
        return voicesArray;
    }

    function getNextQuestionTextVoiceObject($element) {
        var voicesArray = [];

        const text = presenter.config.nextLabel;
        pushMessageToTextVoiceObject(voicesArray, text, true);

        if (!$element.hasClass(CSS_CLASSES.ACTIVE))
            pushMessageToTextVoiceObject(voicesArray, presenter.speechTexts.Inactive);

        return voicesArray;
    }
    
    function getCommentFieldTextVoiceObject($element) {
        var voicesArray = [];

        const commentField = presenter.speechTexts.CommentField;
        pushMessageToTextVoiceObject(voicesArray, commentField);

        if ($element.children().length === 0)
            return updateCommentFieldTextVoiceObjectWithInactive(voicesArray);

        const $gameMessage = $(presenter.getGameMessageElementForKeyboardNavigationFromElement($element));
        if ($gameMessage.length === 0)
            return updateCommentFieldTextVoiceObjectWithHint(voicesArray, $element);
        return updateCommentFieldTextVoiceObjectWithSummary(voicesArray, $gameMessage);
    }

    function updateCommentFieldTextVoiceObjectWithInactive(textVoiceObject) {
        var voicesArray = [];
        const inactiveMessage = presenter.speechTexts.Inactive;
        pushMessageToTextVoiceObject(voicesArray, inactiveMessage);
        return textVoiceObject.concat(voicesArray);
    }

    function updateCommentFieldTextVoiceObjectWithSummary(
            textVoiceObject, $gameMessageElement) {
        var voicesArray = [];
        const summaryMessage = presenter.speechTexts.Summary;
        pushMessageToTextVoiceObject(voicesArray, summaryMessage);

        const $gameMessageChildren = $gameMessageElement.children();
        
        var $gameMessageText = null;
        var gameSummaryChildID = 1;
        if ($gameMessageChildren.length === 2) {
            $gameMessageText = $($gameMessageChildren.get(0));
        } else {
            $gameMessageText = getTextNodeFromElement($gameMessageElement);
            gameSummaryChildID = 0;
        }
        voicesArray = createAndConcatElementWithTextVoiceObject(
            voicesArray, $gameMessageText);
        const $gameSummary = $($gameMessageChildren.get(gameSummaryChildID));
        
        const $gameSummaryText = getTextNodeFromElement($gameSummary);
        voicesArray = createAndConcatElementWithTextVoiceObject(
            voicesArray, $gameSummaryText);

        voicesArray = createAndConcatElementsWithTextVoiceObject(
            voicesArray, $gameSummary.children());

        return textVoiceObject.concat(voicesArray);
    }

    function updateCommentFieldTextVoiceObjectWithHint(
            textVoiceObject, $commendFieldElement) {
        var voicesArray = [];

        const hintMessage = presenter.speechTexts.Hint;
        pushMessageToTextVoiceObject(voicesArray, hintMessage);

        voicesArray = createAndConcatElementWithTextVoiceObject(
            voicesArray, $commendFieldElement);
        return textVoiceObject.concat(voicesArray);
    }

    QuizKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    QuizKeyboardController.prototype.select = function (event) {
        var currentElement = this.keyboardNavigationCurrentElement;
        var $element = this.getTarget(currentElement, false);
        
        if ($element.hasClass(CSS_CLASSES.QUESTION_TIP))
            selectQuestionTip(event, $element);
        else if ($element.hasClass(CSS_CLASSES.FIFTY_FIFTY))
            selectFiftyFiftyButton(event);
        else if ($element.hasClass(CSS_CLASSES.HINT_BUTTON))
            selectHintButton(event);
        else KeyboardController.prototype.select.call(this, event);
    };

    function selectQuestionTip(event, $element) {
        const isQuestionAnsweredBefore = isQuestionAnswered(); 
        KeyboardController.prototype.select.call(presenter.keyboardControllerObject, event);
        const isQuestionAnsweredAfter = isQuestionAnswered();

        if (isQuestionAnsweredBefore !== isQuestionAnsweredAfter) {
            var voicesArray = updateQuestionTipTextVoiceObjectWithSelected([], $element);
            const $gameMessage = presenter.getGameMessageElementForKeyboardNavigationFromElement(presenter.$view);
            if ($gameMessage.length !== 0) {
                voicesArray = updateCommentFieldTextVoiceObjectWithSummary(voicesArray, $gameMessage);
            }
            
            if (!isLastQuestion() 
                &amp;&amp; presenter.config.nextAfterSelect 
                &amp;&amp; presenter.isWCAGOn) {
                presenter.speakWithCallback(
                    voicesArray, nextAfterSelectCallback);
            } else {
                presenter.speak(voicesArray);
            }
        }
    }

    function isQuestionAnswered() {
        return state.selectedAnswer !== null;
    }

    function selectFiftyFiftyButton(event) {
        const fiftyFiftyUsedBefore = state.fiftyFiftyUsed;
        KeyboardController.prototype.select.call(presenter.keyboardControllerObject, event);
        const fiftyFiftyUsedAfter = state.fiftyFiftyUsed;

        if (fiftyFiftyUsedBefore !== fiftyFiftyUsedAfter) {
            var voicesArray = [];
            const selectedMessage = presenter.speechTexts.Selected;
            pushMessageToTextVoiceObject(voicesArray, selectedMessage);
            const $questionTips = $(presenter.getQuestionTipsElementsForKeyboardNavigation());
            for (var i = 0; i &lt; state.answersOrder.length; i++) {
                if (state.answersOrder[i] === null) {
                    const $questionTip = $($questionTips.get(i));
                    voicesArray = voicesArray.concat(getQuestionTipTextVoiceObject($questionTip));
                }
            }
            presenter.speak(voicesArray);
            presenter.keyboardControllerObject.refreshMarkOnCurrentElement();
        }
    }

    function selectHintButton(event) {
        const hintUsedBefore = state.hintUsed;
        KeyboardController.prototype.select.call(presenter.keyboardControllerObject, event);
        const hintUsedAfter = state.hintUsed;

        if (hintUsedBefore !== hintUsedAfter) {
            var voicesArray = [];
            const selectedMessage = presenter.speechTexts.Selected;
            pushMessageToTextVoiceObject(voicesArray, selectedMessage);

            const $commendFieldElement = $(presenter.getCommendFieldElementForKeyboardNavigation());
            voicesArray = updateCommentFieldTextVoiceObjectWithHint(voicesArray, $commendFieldElement);
            presenter.speak(voicesArray);
        }
    }

    function getTextNodeFromElement($element) {
        return $element.contents().filter(
            function () {
                return this.nodeType === 3;
            }
        )
    }

    function createAndConcatElementsWithTextVoiceObject(textVoiceObject, $elements, usePresenterLangTag = true) {
        $elements.each( function() { 
            textVoiceObject = createAndConcatElementWithTextVoiceObject(textVoiceObject, $(this), usePresenterLangTag);
        });
        return textVoiceObject;
    }

    function createAndConcatElementWithTextVoiceObject(textVoiceObject, $element, usePresenterLangTag = true) {
        var elementTextVoiceArray = null;
        if (usePresenterLangTag)
            elementTextVoiceArray = window.TTSUtils.getTextVoiceArrayFromElement($element, presenter.config.langTag)
        else
            elementTextVoiceArray = window.TTSUtils.getTextVoiceArrayFromElement($element)
        return textVoiceObject.concat(elementTextVoiceArray);
    }

    function pushMessagesToTextVoiceObject(textVoiceObject, messages, usePresenterLangTag = false) {
        for (var i = 0; i &lt; messages.length; i++) {
            pushMessageToTextVoiceObject(textVoiceObject, messages[i], usePresenterLangTag);
        }
    }
    
    function pushMessageToTextVoiceObject(textVoiceObject, message, usePresenterLangTag = false) {
        if (usePresenterLangTag)
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, presenter.config.langTag));
        else
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="SVG2" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="SVG file" nameLabel="SVG2_property_svg_file" type="image"/>
		<property name="Skip aspect ratio" nameLabel="SVG2_property_skip_aspect_ration" type="boolean"/>
	</model>
<css>.addon_SVG2 .errorContainer {
    border: 1px dotted red;
    display: none;
}
.addon_SVG2 .svgContainer {
    border: 1px dotted lightgray;
    overflow: hidden;
    width: 100%;
    height: 100%;
}
.addon_SVG2 .errorContainer .message{
    padding: 5px;
    color: red;
}
</css><view>&lt;div class="errorContainer"&gt;
    &lt;div class="message"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="svgContainer"&gt;&lt;/div&gt;
</view><preview>&lt;div class="errorContainer"&gt;
    &lt;div class="message"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="svgContainer"&gt;&lt;/div&gt;
</preview><presenter>function AddonSVG2_create(){
    var presenter = function(){};

    var svgContainer, errorContainer, aspect, containerWidth, containerHeight;

    var errorMessages = {
        invalidFile: 'Invalid SVG file',
        svgSupportMissing: 'Your browser doesn\'t support SVG',
        xmlSerializerNotSupported: 'XmlSerializer not supported',
        missingFile: 'Please provide SVG file'
    };

    presenter.run = function(view, model){
        presenter.presenterLogic(view, model);
        presenter.setVisibility(presenter.isVisibleByDefault);
    };

    presenter.presenterLogic = function (view, model) {
        presenter.$view = $(view);
        svgContainer = $(view).find('.svgContainer:first');
        errorContainer = $(view).find('.errorContainer');
        aspect = model['Skip aspect ratio'] == 'True';
        containerWidth = model['Width'];
        containerHeight = model['Height'];
        presenter.isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.isVisibleByDefault = presenter.isVisible;

        if(this.hasSVGSupport()) {
            if(model['SVG file'] != '') {
                this.loadFile(model['SVG file']);
            } else {
                this.onError(errorMessages.missingFile);
            }
        } else {
            this.onError(errorMessages.svgSupportMissing);
        }
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    //detection based on Modernizer library
    presenter.hasSVGSupport = function() {
        return !!document.createElementNS &amp;&amp; !!document.createElementNS('http://www.w3.org/2000/svg', "svg").createSVGRect;
    };

    presenter.loadFile = function(file) {
        $.ajax({
            url: file,
            success: this.onLoadComplete,
            error: this.onLoadError,
            dataType: 'xml'
        });
    };

    presenter.onLoadComplete = function(data) {
        var el = $(data).find('svg');
        //validate response
        if(el.length === 0) {
            presenter.onError(errorMessages.invalidFile);
            return;
        }

        var cw = el.attr('width') ? el.attr('width') : containerWidth;
        var ch = el.attr('height') ? el.attr('height') : containerHeight;

        //fit size of svg
        cw = cw.replace("px", "");
        ch = ch.replace("px", "");

        el.attr('viewBox', '0 0 ' + cw + ' ' + ch);
        el.attr('preserveAspectRatio', !aspect ? 'xMinYMin' : 'none');
        el.attr('width', '100%');
        el.attr('height', '100%');

        svgContainer.html('');
        //convert xml to string
        var svgHtml = presenter.xmlToString(data);
        if(svgHtml === false) {
            presenter.onError(errorMessages.xmlSerializerNotSupported);
        } else {
            //add converted content
            svgContainer.append(svgHtml);
        }
    };

    presenter.xmlToString = function(xml) {
        var output = false;
        try {
            // Gecko-based browsers, Safari, Opera
            output = new XMLSerializer().serializeToString(xml);
        } catch(e) {
            try {
                // Internet Explorer
                output = xml.xml;
            } catch(e2) {
                //Xmlserializer not supported
                return false;
            }
        }
        return output;
    };

    presenter.onLoadError = function() {
        presenter.onError(errorMessages.invalidFile);
    };

    presenter.onError = function(msg) {
        svgContainer.hide();
        errorContainer.find('.message').html(msg);
        errorContainer.show();
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.reset = function() {
        presenter.isVisibleByDefault ? presenter.show() : presenter.hide();
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.isVisible
        });
    };

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state);
        presenter.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.isVisible);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Scoreboard" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
      <property displayName="Broadcast" isLocalized="false" name="Broadcast" nameLabel="Scoreboard_broadcast" type="string"/>
      <property displayName="isDraggable" isLocalized="false" name="isDraggable" nameLabel="Scoreboard_is_draggable" type="boolean"/>
      <property displayName="VariableStorageLocation" name="VariableStorageLocation" nameLabel="Scoreboard_location_of_variable_storage_module" type="{header, footer}"/>
      <property displayName="VariableStorageLocationName" isLocalized="false" name="VariableStorageLocationName" nameLabel="Scoreboard_variable_storage_location_name" type="string"/>
      <property displayName="maximumTeamsCount" isLocalized="false" name="maximumTeamsCount" nameLabel="Scoreboard_maximum_teams_count" type="string"/>
      <property displayName="initialTeamsCount" isLocalized="false" name="initialTeamsCount" nameLabel="Scoreboard_initial_teams_count" type="string"/>
      <property isDefault="true" name="defaultTeamsList" nameLabel="Scoreboard_default_teams_list" type="list">
        <property name="teamName" nameLabel="Scoreboard_property_name" type="string"/>
        <property name="teamColor" nameLabel="Scoreboard_property_color" type="string"/>
      </property>
    </model>
<css>.addon_Scoreboard {
    position: relative;
    overflow-y: auto;
    overflow-x: hidden;
}

.scoreboard-container {
    display: inline-block;
    background-color: rgba(255, 255, 255, 1);
    border-radius: 4px;
}

.scoreboard-header {
    height: 20px;
    line-height: 20px;
    position: relative;
    background-color: rgba(128, 207, 238, 0.973);
}

.ui-draggable .scoreboard-header {
    cursor: move;
}

.scoreboard-header .scoreboard-close {
    width: 10%;
    right: 5px;
    float: right;
    cursor: pointer;
    text-align: center;
}

.scoreboard-close::before {
    content: "x";
}

.scoreboard-footer {
    height: 20px;
    width: 100%;
    color:rgba(0, 0, 0, 0.5);
    cursor: move;
    position: relative;
    overflow: hidden;
    bottom: 0;
}

.scoreboard-body {
    text-align: left;
    display: inline-block;
}

.scoreboard-footer .scoreboard-add-new-team {
    width: 40%;
    right: 5px;
    float: right;
    cursor: pointer;
    text-align: center;
}

.scoreboard-add-new-team::before {
    content: "+";
}

.scoreboard-footer .scoreboard-reset {
    width: 40%;
    right: 5px;
    float: left;
    cursor: pointer;
    text-align: center;
}

.scoreboard-reset::before {
    content: "\21B6";
}

.scoreboard-team {
    padding: 2px;
    margin: 3px;
    width: 100px;
    height: 100%;
    display: inline-block;
    text-align: center;
    border-radius: 4px;
    color: rgba(255, 255, 255, 1);
}

.scoreboard-team-header {
    height: 15px;
    line-height: 15px;
}

.scoreboard-team-header .scoreboard-team-remove {
    width: 10%;
    right: 5px;
    float: right;
    cursor: pointer;
    text-align: right;
}

.scoreboard-team-remove::before {
    content: "x";
}

.scoreboard-save-button {
    cursor: pointer;
}

.scoreboard-team-body {
    height: 70px;
}

.scoreboard-team-body .scoreboard-team-name-container {
    vertical-align: top;
}

.scoreboard-team-body .scoreboard-team-name-container .scoreboard-team-name-input {
    width: 90px;
    background: transparent;
    color: white;
    border: none;
}

.scoreboard-team-body .scoreboard-team-points-container {
    margin-top: 15px;
}

.scoreboard-team-footer {
    height: 25px;
    width: 100%;
    line-height: 25px;
    bottom: 0;
}

.scoreboard-team-footer .scoreboard-team-score-decrement {
    width: 50%;
    height: 100%;
    cursor: pointer;
    display: inline-block;
}

.scoreboard-team-score-decrement::before {
    content: "-";
}

.scoreboard-team-footer .scoreboard-team-score-increment {
    width: 50%;
    height: 100%;
    cursor: pointer;
    display: inline-block;
}

.scoreboard-team-score-increment::before {
    content: "+";
}
</css><view/><preview/><presenter>function AddonScoreboard_create() {
    var presenter = function() {};

    presenter.teamsObjects = [];
    presenter.scoreboard = null;

    presenter.state = {
        isVisible: false,
        teamsObjects: [],
        savedScoreboard: null,
    }
    presenter.playerController = null;

    presenter.ERROR_CODES = {
        C01: 'Configuration cannot be empty',
        I01: 'Initial number of teams is greater than maximum number of teams!',
        I02: 'Initial number of teams must be positiv integer!',
        I03: 'Initial team name cannot be empty',
        I04: 'Team color must be in RGB format (hexadecimal) and start with #',
        I05: 'Maximum number of teams must be positiv integer!',
    };

    presenter.run = function (view, model) {
        presenterLogic(view, model, false);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    function deleteCommands() {
        delete presenter.getState;
        delete presenter.setState;
    }

    function presenterLogic(view, model, isPreview) {
        presenter.configuration = presenter.validateModel(model);

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);

            deleteCommands();
            return;
        }

        presenter.initView(view, model);
        
    };

    presenter.validateMaximumTeamsCount = function (number) {
        var validatedNumber = ModelValidationUtils.validateInteger(number)
        if (!validatedNumber.isValid || validatedNumber.value &lt; 1) {
            return ModelValidationUtils.getErrorObject('I05');
        }

        return validatedNumber;
    };

    presenter.validateInitialTeamsCount = function (number, maxTeamsCount) {
        var validatedNumber = ModelValidationUtils.validateInteger(number)
        if (!validatedNumber.isValid || validatedNumber.value &lt; 1) {
            return ModelValidationUtils.getErrorObject('I02');
        }

        if (validatedNumber.value &gt; maxTeamsCount) {
            return ModelValidationUtils.getErrorObject('I01');
        }
        return validatedNumber;
    };

    presenter.validateInitialTeams = function (initialTeams, maxTeamsCount) {
        var validatedInitialTeams = [], i;
        var regExp = new RegExp("#[0-9a-fA-F]+");
        
        if (initialTeams.length &gt; maxTeamsCount) {
            return ModelValidationUtils.getErrorObject('I01');
        }

        for (i = 0; i &lt; maxTeamsCount; i++) {
            var initialTeam = initialTeams[i];

            if(!initialTeam) {
                initialTeam = {}
                initialTeam.teamName = "X";
                initialTeam.teamColor = "#000";
            }

            if(ModelValidationUtils.isStringEmpty(initialTeam.teamName)) {
                return ModelValidationUtils.getErrorObject('I03');
            }

            var teamColor = initialTeam.teamColor;
            var colorMatch;
            if (!teamColor) {
                teamColor = "#000";
            } else {
                if (teamColor.length &lt; 4 || teamColor.length &gt; 7) {
                    return ModelValidationUtils.getErrorObject('I04');
                }
                colorMatch = teamColor.match(regExp);
                if (!colorMatch || colorMatch === null || colorMatch.length &lt; 1) {
                    return ModelValidationUtils.getErrorObject('I04');
                }
                if (colorMatch[0].length &lt; teamColor.length) {
                    return ModelValidationUtils.getErrorObject('I04');
                }
            }
            var initialTeamObject = {
                'teamId': i,
                'teamName': initialTeam.teamName,
                'teamPoints': 0,
                'teamColor': initialTeam.teamColor,
            }

            validatedInitialTeams.push(initialTeamObject)
        };

        return {
            isValid: true,
            validatedInitialTeams: validatedInitialTeams
        }
    };

    presenter.validateModel = function (model) {
        var isOnePageScoreboard = !model['Broadcast'];

        if (model['Broadcast'] !== "" &amp;&amp; (ModelValidationUtils.isStringEmpty(model['VariableStorageLocation']) ||
            ModelValidationUtils.isStringEmpty(model['VariableStorageLocationName']))) {
            return ModelValidationUtils.getErrorObject('C01');
        }

        var validatedMaximumTeamsCount = presenter.validateMaximumTeamsCount(model['maximumTeamsCount']);
        if (!validatedMaximumTeamsCount.isValid) {
            return validatedMaximumTeamsCount;
        }

        var validatedInitialTeamsCount = presenter.validateInitialTeamsCount(model['initialTeamsCount'], validatedMaximumTeamsCount.value);
        if (!validatedInitialTeamsCount.isValid) {
            return validatedInitialTeamsCount;
        }

        var validatedInitialTeams = presenter.validateInitialTeams(model['defaultTeamsList'], validatedMaximumTeamsCount.value);
        if (!validatedInitialTeams.isValid) {
            return validatedInitialTeams;
        }

        return {
            isValid: true,
            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            broadcast: model['Broadcast'],
            isDraggable: ModelValidationUtils.validateBoolean(model['isDraggable']),
            variableStorageLocation: model['VariableStorageLocation'],
            variableStorageLocationName: model['VariableStorageLocationName'],
            isOnePageScoreboard: isOnePageScoreboard,
            defaultTeamsList: validatedInitialTeams.validatedInitialTeams,
            maximumTeamsCount: validatedMaximumTeamsCount.value,
            initialTeamsCount: validatedInitialTeamsCount.value
        }
    };

    presenter.initView = function (view, model) {
        presenter.view = view;
        presenter.$view = $(view);
        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
        presenter.scoreboard = presenter.createScoreboard(presenter.$view);
        for (var i = 0; i &lt; presenter.configuration.initialTeamsCount; i++) {
            presenter.scoreboard = presenter.addTeam(presenter.configuration.defaultTeamsList[i], presenter.scoreboard);
        }
        presenter.setVisibility(presenter.configuration.isVisible);
    }

// Creating scoreboard
    presenter.Scoreboard = function scoreboardCreate () {
        this.$scoreboard = null;
        this.$scoreboardBody = null;
        this.$scoreboardHeader = null;
        this.$closeButton = null;
        this.$scoreboardFooter = null;
        this.$addNewTeamButton = null;
        this.$resetButton = null;
    };

    presenter.Scoreboard._internals = {};

    presenter.Scoreboard._internals.createView = function scoreboardCreateView (savedScoreboard) {
        this.$scoreboard = savedScoreboard;
        this.$scoreboardBody = $('&lt;div class="scoreboard-body"&gt;&lt;/div&gt;');
        this.$scoreboardHeader = $('&lt;div class="scoreboard-header"&gt;&lt;/div&gt;');
        this.$closeButton = $('&lt;div class="scoreboard-close"&gt;&lt;/div&gt;');
        this.$scoreboardFooter = $('&lt;div class="scoreboard-footer"&gt;&lt;/div&gt;');
        this.$resetButton = $('&lt;div class="scoreboard-reset"&gt;&lt;/div&gt;');
        this.$addNewTeamButton = $('&lt;div class="scoreboard-add-new-team"&gt;&lt;/div&gt;');

        this.$scoreboardHeader.append(this.$closeButton);

        this.$scoreboardFooter.append(this.$resetButton);
        this.$scoreboardFooter.append(this.$addNewTeamButton);
        
        this.$scoreboard.append(this.$scoreboardHeader);
        this.$scoreboard.append(this.$scoreboardBody);
        this.$scoreboard.append(this.$scoreboardFooter);

    };

    presenter.Scoreboard.prototype.init = function scoreboardInit (savedScoreboard) {
        presenter.Scoreboard._internals.createView.call(this, savedScoreboard);

        this.connectHandlers();
        if (presenter.configuration.isDraggable) {
            this.connectDraggable(savedScoreboard);
        }
    };

    presenter.Scoreboard.prototype.setBody = function scoreboardSetBody (body) {
        this.$scoreboardBody.append(body);
    };

    presenter.Scoreboard.createScoreboard = function creatingScoreboard (savedScoreboard) {
        var scoreboard = new presenter.Scoreboard(savedScoreboard);
        scoreboard.init(savedScoreboard);
        
        return scoreboard;
    };

    presenter.createScoreboard = function scoreboardCreateScoreboard (savedScoreboard) {
        return presenter.Scoreboard.createScoreboard(savedScoreboard);
    }

    presenter.Scoreboard.prototype.getState = function scoreboardGetState () {
        return {
            'top': this.$scoreboard.css('top'),
            'left': this.$scoreboard.css('left'),
        };
    };

    presenter.Scoreboard.prototype.getView = function scoreboardGetView () {
        return this.$scoreboard;
    };

    // Scoreboard buttons
    presenter.Scoreboard.prototype.closeButtonHandler = function scoreboardCloseButtonHandler (event) {
        presenter.closeScoreBoard();
    };

    presenter.Scoreboard.prototype.resetButtonHandler = function scoreboardResetButtonHandler (event) {
        presenter.resetTeamsPoints();
    };

    function getLowestAvaibleTeamId() {
        var freeIds = [];
        var workingIds = []
        presenter.teamsObjects.forEach(function (team) {
            workingIds.push(team.getTeamId().teamId);
        });
        for (var i = 0; i &lt; presenter.configuration.defaultTeamsList.length; i++) {
            if (!workingIds.includes(i)) {
                freeIds.push(i);
            }      
        }
        return Math.min(...freeIds);
    }

    presenter.Scoreboard.prototype.addTeamButtonHandler = function scoreboardResetButtonHandler (event) {
        if (presenter.state.teamsObjects.length &lt; presenter.configuration.maximumTeamsCount) {
            var availableLowestId = getLowestAvaibleTeamId();
            var defaultTeamData = presenter.configuration.defaultTeamsList[availableLowestId]
            presenter.addTeam(defaultTeamData, this);
        }
    };

    presenter.Scoreboard.prototype.connectHandlers = function scoreboardConnectHandlers () {
        if (MobileUtils.isEventSupported('touchstart')) {
            this.$closeButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.closeButtonHandler(event);
            }.bind(this));
            this.$resetButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.resetButtonHandler(event);
            }.bind(this));
            this.$addNewTeamButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.addTeamButtonHandler(event);
            }.bind(this));
        } else {
            this.$closeButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.closeButtonHandler(event);
            }.bind(this));
            this.$resetButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.resetButtonHandler(event);
            }.bind(this));
            this.$addNewTeamButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.addTeamButtonHandler(event);
            }.bind(this));
        }
    };

    presenter.Scoreboard.prototype.connectDraggable = function scoreboardConnectDraggable (savedScoreboard) {
        this.$scoreboard.draggable({
            containment: 'parent',
            opacity: 0.35,
            create: function(event, _) {
                $(event.target).css({
                    'top' : savedScoreboard ? savedScoreboard.top : '10px',
                    'left' : savedScoreboard ? savedScoreboard.left : '10px',
                    'position' : 'absolute'
                });
            },

            stop: function ScoreboardStopFunction() {
                $.ui.ddmanager.current = null;
            }
        });
    };

    presenter.Scoreboard.prototype.moveScoreboard = function scoreboardMoveScoreboard (savedScoreboardPosition) {
        var ic_page_height = this.$scoreboard.parent().height();
        this.$scoreboard.css({
            'top' : parseInt(savedScoreboardPosition.top, 10) &lt; ic_page_height ? savedScoreboardPosition.top : '10px',
            'left' : savedScoreboardPosition.left,
        });
    };

    presenter.Scoreboard.prototype.destroy = function scoreboardDestroy () {
        if(this.$scoreboard) {
            var $scoreboard = this.$scoreboard.draggable("destroy");
            $scoreboard.off();
            this.$scoreboard.off();
            this.$scoreboardBody.off();
            this.$scoreboardHeader.off();
            this.$closeButton.off();
            this.$scoreboardFooter.off();
            this.$resetButton.off();
            this.$addNewTeamButton.off();
            this.$scoreboard = null;
            this.$scoreboardBody = null;
            this.$scoreboardHeader = null;
            this.$closeButton = null;
            this.$scoreboardFooter = null;
            this.$resetButton = null;
            this.$addNewTeamButton = null;
        }
    };

// Creating Team
    presenter.Team = function TeamCreate (savedTeams) {
        this.teamId = savedTeams.teamId;
        this.teamPoints = savedTeams.teamPoints;
        this.teamName = savedTeams.teamName;
        this.teamColor = savedTeams.teamColor;
        this.$team = null;
        this.$teamHeader = null;
        this.$teamBody = null;
        this.$teamFooter = null;
        this.$teamPointsIncrementButton = null;
        this.$teamPointsDecrementButton = null;
        this.$teamNameContainer = null;
        this.$teamPointsConatiner = null;
        this.$teamNameInput = null;
        this.$teamRemoveButton = null;
    };

    presenter.Team._internals = {};

    presenter.Team._internals.createView = function TeamCreateView (savedTeam) {
        this.$team = $('&lt;div class="scoreboard-team"&gt;&lt;/div&gt;');
        this.$teamHeader = $('&lt;div class="scoreboard-team-header"&gt;&lt;/div&gt;');
        this.$teamBody = $('&lt;div class="scoreboard-team-body"&gt;&lt;/div&gt;');
        this.$teamFooter = $('&lt;div class="scoreboard-team-footer"&gt;&lt;/div&gt;');
        this.$teamRemoveButton = $('&lt;div class="scoreboard-team-remove"&gt;&lt;/div&gt;');
        this.$teamNameContainer = $(
            `&lt;div class="scoreboard-team-name-container"&gt;
                ${this.teamName}
            &lt;/div&gt;`
        );
        this.$teamPointsConatiner = $(
            `&lt;div class="scoreboard-team-points-container"&gt;
                &lt;h1&gt;${this.teamPoints}&lt;/h1&gt;
            &lt;/div&gt;`
        );
        this.$teamPointsDecrementButton = $(
        '&lt;div class="scoreboard-team-score-decrement"&gt;&lt;/div&gt;'
        );
        this.$teamPointsIncrementButton = $(
        '&lt;div class="scoreboard-team-score-increment"&gt;&lt;/div&gt;'
        );
        this.$teamHeader.append(this.$teamRemoveButton);

        this.$teamBody.append(this.$teamNameContainer);
        this.$teamBody.append(this.$teamPointsConatiner);
        
        this.$teamFooter.append(this.$teamPointsDecrementButton);
        this.$teamFooter.append(this.$teamPointsIncrementButton);

        this.$team.css("background-color", savedTeam.teamColor);

        this.$team.append(this.$teamHeader);
        this.$team.append(this.$teamBody);
        this.$team.append(this.$teamFooter);
    };

    presenter.Team.prototype.init = function teamInit (savedTeam) {
        presenter.Team._internals.createView.call(this, savedTeam);

        this.connectHandlers();
    };

    presenter.Team.createTeam = function teamCreateTeam (savedTeam) {
        var team = new presenter.Team(savedTeam);
        team.init(savedTeam);
        
        return team;
    };

    // Scoreboard buttons
    presenter.Team.prototype.resetPoints = function teamResetPoints () {
        this.teamPoints = 0;
        this.$teamPointsConatiner.html(`&lt;h1&gt;${this.teamPoints}&lt;/h1&gt;`)
    };

    presenter.Team.prototype.removeTeamHandler = function teamRemoveTeamHandler (event) {
        var team = event.data.scoreboard;
        team.destroy();
    };

    presenter.Team.prototype.incrementPointsButtonHandler = function teamIncrementPointsButtonHandler (event) {
        this.teamPoints += 1;
        this.$teamPointsConatiner.html(`&lt;h1&gt;${this.teamPoints}&lt;/h1&gt;`)
        presenter.updateTeamState(this.getTeamData());
    };

    presenter.Team.prototype.decrementPointsButtonHandler = function teamDecrementPointsButtonHandler (event) {
        this.teamPoints -= 1;
        this.$teamPointsConatiner.html(`&lt;h1&gt;${this.teamPoints}&lt;/h1&gt;`)
        presenter.updateTeamState(this.getTeamData());
    };

    presenter.Team.prototype.connectHandlers = function teamConnectHandlers () {
        if(MobileUtils.isEventSupported('touchstart')) {
            this.$teamRemoveButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.removeTeamHandler(event);
            }.bind(this));
            this.$teamPointsIncrementButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.incrementPointsButtonHandler(event);
            }.bind(this));
            this.$teamPointsDecrementButton.on('touchstart', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.decrementPointsButtonHandler(event);
            }.bind(this));
        } else {
            this.$teamRemoveButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.removeTeamHandler(event);
            }.bind(this));
            this.$teamPointsIncrementButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.incrementPointsButtonHandler(event);
            }.bind(this));
            this.$teamPointsDecrementButton.on('click', {"scoreboard": this}, function (event) {
                event.stopPropagation();
                this.decrementPointsButtonHandler(event);
            }.bind(this));
        }
        
        this.connectTeamNameEditHandler();
    };

    presenter.Team.prototype.connectTeamNameEditHandler = function teamConnectTeamNameEditHandler () {
        this.$teamNameContainer.on('dblclick', function () {
            this.teamNameEditHandler();
            this.$teamNameContainer.off('dblclick');
        }.bind(this));

        if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
            window.EventsUtils.DoubleTap.on(this.$teamNameContainer, function () {
                this.teamNameEditHandler();
                window.EventsUtils.DoubleTap.off(this.$teamNameContainer);
            }.bind(this));
        }
    };

    presenter.Team.prototype.teamNameEditHandler = function teamNameEditHandler () {
        this.currentValue = this.teamName;

        this.$teamNameInput = $('&lt;input class="team-name-input"&gt;&lt;/input&gt;');
        this.$teamNameInput.val(this.currentValue);
        
        this.$teamNameContainer.html(this.$teamNameInput);
        this.$teamNameInput.focus();

        this.$teamNameInput.keypress(function (e) {
            if (e.which == 13) {
                this.saveNewTeamName();
                return false;
            }
        }.bind(this));
        this.$teamNameInput.focusout(function (e) {
            this.saveNewTeamName();
            return false;
        }.bind(this));
    };

    presenter.Team.prototype.saveNewTeamName = function teamSaveNewTeamName () {
        var value = this.$teamNameInput.val();
        this.teamName = value;
        this.$teamNameContainer.html(value);
        this.$teamNameInput.remove();
        this.connectTeamNameEditHandler();
        presenter.updateTeamState(this.getTeamData());
    };

    presenter.Team.prototype.getView = function teamGetView () {
        return this.$team;
    };

    presenter.Team.prototype.getTeamData = function teamGetView () {
        return {
            'teamId': this.teamId,
            'teamName': this.teamName,
            'teamPoints': this.teamPoints,
            'teamColor': this.teamColor,
        };
    };

    presenter.Team.prototype.getTeamId = function teamGetTeamId () {
        return {
            'teamId': this.teamId,
        };
    };

    presenter.Team.prototype.destroy = function teamDestroy () {
        if(this.$team) {
            presenter.removeTeam(this.teamId);
            var $team = this.$team.draggable("destroy");
            $team.off();
            this.$team.off();
            this.$teamHeader.off();
            this.$teamBody.off();
            this.$teamFooter.off();
            this.$teamRemoveButton.off();
            this.$teamNameContainer.off();
            this.$teamPointsConatiner.off();
            this.$teamPointsDecrementButton.off();
            this.$teamPointsIncrementButton.off();
            if (this.$teamNameInput !== null) {
                this.$teamNameInput.off();
            }

            if (this.$teamRemoveButton !== null) {
                this.$teamRemoveButton.off();
            }

            window.EventsUtils.DoubleTap.off(this.$teamNameContainer);
            this.$team.remove();
            this.$team = null;
            this.teamPoints = 0;
            this.teamName = '';
            this.teamColor = ''
            this.$teamHeader = null;
            this.$teamBody = null;
            this.$teamFooter = null;
            this.$teamPointsIncrementButton = null;
            this.$teamPointsDecrementButton = null;
            this.$teamNameContainer = null;
            this.$teamPointsConatiner = null;
            this.$teamNameInput = null;
            this.$teamRemoveButton = null;
        }
    };

    presenter.createTeam = function Scoreboard_createTeam (savedTeam) {
        return presenter.Team.createTeam(savedTeam)
    };

    presenter.resetTeamsPoints = function Scoreboard_resetTeamPoints () {
        presenter.teamsObjects.forEach(function (team) {
            team.resetPoints();
        });
        presenter.state.teamsObjects.forEach(function (team) {
            team.teamPoints = 0;
        });
    };

    presenter.removeTeam = function Scoreboard_removeTeam (teamId) {
        presenter.teamsObjects = presenter.teamsObjects.filter((team) =&gt; {
            return teamId !== team.getTeamId().teamId;
        });
        presenter.state.teamsObjects = presenter.state.teamsObjects.filter((team) =&gt; {
            return teamId !== team.teamId;
        });
    };

    presenter.addTeam = function Scoreboard_addTeam (teamData, scoreboard) {
        var team = presenter.createTeam(teamData);
        presenter.teamsObjects.push(team);
        presenter.state.teamsObjects.push(team.getTeamData());
        var $teamView = team.getView();
        scoreboard.setBody($teamView);
        return scoreboard;
    };
    
    presenter.removeAllTeams = function Scoreboard_removeAllTeams () {
        presenter.teamsObjects.forEach(function (team) {
            team.destroy();
            team = null;
        });
    };

    presenter.removeScoreboard = function Scoreboard_removeScoreboard () {
        presenter.scoreboard.destroy();
        presenter.scoreboard = null;

    }

    presenter.updateTeamState = function Scoreboard_updateTeamState (updatedTeamData) {
        presenter.state.teamsObjects = presenter.state.teamsObjects.map(teamData =&gt;
            teamData.teamId === updatedTeamData.teamId ? { 
                ...teamData,
                'teamName': updatedTeamData.teamName,
                'teamPoints': updatedTeamData.teamPoints,
            } : teamData
        );
    };

    presenter.restoreAllScoreboardData = function Scoreboard_restoreAllScoreboardData (savedScoreboard) {
        presenter.removeAllTeams();
        presenter.scoreboard.moveScoreboard(savedScoreboard.scoreboard);
        savedScoreboard.teamsObjects.forEach(function (teamData) {
            presenter.addTeam(teamData, presenter.scoreboard);
        });
        presenter.state.isVisible = savedScoreboard.isVisible;
        presenter.setVisibility(presenter.state.isVisible);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.closeScoreBoard = function () {
        var eventData = {
            'source': presenter.configuration.ID,
            'value': "HideScoreboard",
        };
        var eventBus = presenter.playerController.getEventBus();
        eventBus.sendEvent('ValueChanged', eventData);
        presenter.setVisibility(false);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.executeCommand = function (name) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, [], presenter);
    };

    presenter.setPlayerController = function Scoreboard_setPlayerController (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();

        presenter.eventBus.addEventListener('PageLoaded', this);
    };

    presenter.onEventReceived = function Scoreboard_onEventReceived (eventName, eventData) {
        if (presenter.configuration.isOnePageScoreboard) {
            return;
        }
        if (eventName == "PageLoaded" &amp;&amp; eventData.source == presenter.configuration.variableStorageLocationName) {
            var store = null;
            if (presenter.configuration.variableStorageLocation == "header") {
                store = presenter.playerController.getHeaderModule(presenter.configuration.broadcast);
            } else {
                store = presenter.playerController.getFooterModule(presenter.configuration.broadcast);
            }

            if (store == null) {
                return;
            }

            if (store.getVariable("savedScoreboard")) {
                presenter.restoreAllScoreboardData(store.getVariable("savedScoreboard"));
            }
        }
    };

    presenter.getState = function Scoreboard_getState () {
        presenter.state.scoreboard = presenter.scoreboard.getState();
        if (!presenter.configuration.isOnePageScoreboard) {
            var store = null;
            if (presenter.configuration.variableStorageLocation == "header") {
                store = presenter.playerController.getHeaderModule(presenter.configuration.broadcast);
            } else {
                store = presenter.playerController.getFooterModule(presenter.configuration.broadcast);
            }

            if (store == null) {
                return;
            }

            store.setVariable("savedScoreboard", presenter.state);
        } else {
            return JSON.stringify({
                savedScoreboard: presenter.state
            })
        }
    }

    presenter.setState = function Scoreboard_setState (state) {
        if (presenter.configuration.isOnePageScoreboard) {
            var parsedState = JSON.parse(state);
            presenter.restoreAllScoreboardData(parsedState.savedScoreboard);
        }
    }

    presenter.reset = function Scoreboard_reset () {
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.destroy = function Scoreboard_destroy (event) {
        if (event.target !== presenter.view) { return; }

        if(presenter.teamsObjects) {
            presenter.removeAllTeams();
        }
        presenter.playerController = null;
        presenter.removeScoreboard();
        presenter.configuration = null;
        presenter.state = null;
        presenter.playerController = null;
        presenter.eventBus = null;
        presenter.ERROR_CODES = null;
        presenter.$view.unbind();
        presenter.$view = null;
        presenter.view = null;
        presenter.teamsObjects = [];
        presenter.teamsObjects = null;
        presenter.createScoreboard = null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="SelectableAddonsBinder" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="Addons" nameLabel="SelectableAddonsBinder_property_addons" type="text"/>
    </model>
<css/><view/><preview/><presenter>function AddonSelectableAddonsBinder_create(){
    var presenter = function () {};
    presenter.configuration = {};
    function presenterLogic (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addons = (model.Addons).split('\n');
        if (isPreview) return;
        presenter.$view.css('visible', 'hidden');
    };
    presenter.getModule = function (moduleID) {
        return presenter.playerController.getModule(moduleID);
    };
    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('ValueChanged', this);
    };
    presenter.onEventReceived = function (eventName, eventData) {
        var moduleID = eventData.source;
        var matchedModule = presenter.matchEvent(moduleID);
        if (!matchedModule || eventData.value != 1) return;
        for (var i = 0; i &lt; presenter.addons.length; i++) {
            if (presenter.addons[i] != moduleID)
                if(presenter.getModule(presenter.addons[i]) != null &amp;&amp; presenter.getModule(presenter.addons[i]).deselect != undefined)
                    presenter.getModule(presenter.addons[i]).deselect();
        }
    };
    presenter.matchEvent = function (moduleID) {
        for (var i = 0; i &lt; presenter.addons.length; i++) {
            if (presenter.addons[i] == moduleID) return true;
        }
        return false;
    };
    presenter.getModule = function (moduleID) {
        return presenter.playerController.getModule(moduleID);
    };
    presenter.run = function (view, model) {
        presenterLogic(view, model, false);
    };
    presenter.createPreview = function (view, model) {
        presenterLogic(view, model, true);
    };
    presenter.getState = function () {
    };
    presenter.setState = function (stringifiedState) {
    };
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Shape_Tracing" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Shape image" nameLabel="Shape_Tracing_property_shape_image" type="image"/>
        <property name="Show Shape image" nameLabel="Shape_Tracing_property_show_shape_image" type="boolean"/>
        <property name="Hide Shape image on check" nameLabel="Shape_Tracing_property_hide_shape_image_on_check" type="boolean"/>
        <property name="Show Boundaries (editor)" nameLabel="Shape_Tracing_property_show_boundaries_editor" type="boolean"/>
        <property name="Background image" nameLabel="Shape_Tracing_property_background_image" type="image"/>
        <property name="Correct number of lines" nameLabel="Shape_Tracing_property_correct_number_of_lines" type="string"/>
        <property name="Points' coordinates" nameLabel="Shape_Tracing_property_points_coordinates" type="text"/>
        <property displayName="Mind points' order" name="isPointsOrder" nameLabel="Shape_Tracing_property_is_points_order" type="boolean"/>
        <property name="Color" nameLabel="Shape_Tracing_property_color" type="string"/>
        <property displayName="Thickness" name="Pen Thickness" nameLabel="Shape_Tracing_property_pen_thickness" type="string"/>
        <property name="Opacity" nameLabel="Shape_Tracing_property_opacity" type="string"/>
        <property name="Border" nameLabel="Shape_Tracing_property_border" type="string"/>
        <property name="Correct Answer Image" nameLabel="Shape_Tracing_property_correct_answer_image" type="image"/>
    </model>
<css>.addon_Shape_Tracing .outer {
    position: relative;
    width: 100%;
    height: 100%;
}

.addon_Shape_Tracing .outer .drawing,
.addon_Shape_Tracing .outer .drawing-main,
.addon_Shape_Tracing .outer .background,
.addon_Shape_Tracing .outer .shape {
    position: absolute;
    width: 100%;
    height: 100%;
    -ms-touch-action: none;
    touch-action: none;
}
.addon_Shape_Tracing .outer .correctImage {
    position: absolute;
    width: 100%;
    height: 100%;
    -ms-touch-action: none;
    touch-action: none;
}

.addon_Shape_Tracing .outer .drawing,
.addon_Shape_Tracing .outer .drawing-main {
    cursor: crosshair;
}

.shape-tracing-wrapper.outer {
    position: relative;
    width: 100%;
    height: 100%;
}

.shape-tracing-wrapper.outer .drawing,
.shape-tracing-wrapper.outer .drawing-main,
.shape-tracing-wrapper.outer .background,
.shape-tracing-wrapper.outer .shape {
    position: absolute;
    width: 100%;
    height: 100%;
    -ms-touch-action: none;
    touch-action: none;
}

.shape-tracing-wrapper.outer .correctImage {
    position: absolute;
    width: 100%;
    height: 100%;
    -ms-touch-action: none;
    touch-action: none;
}

.shape-tracing-wrapper.outer .drawing,
.shape-tracing-wrapper.outer .drawing-main {
    cursor: crosshair;
}
</css><view>&lt;div class="outer shape-tracing-wrapper"&gt;
    &lt;div class="shape"&gt;&lt;/div&gt;
    &lt;div class="background"&gt;&lt;/div&gt;
    &lt;div class="correctImage"&gt;&lt;/div&gt;
    &lt;canvas class="drawing-main"&gt;&lt;/canvas&gt;
    &lt;canvas class="drawing"&gt;&lt;/canvas&gt;
&lt;/div&gt;
</view><preview>&lt;div class="outer shape-tracing-wrapper"&gt;
    &lt;div class="shape"&gt;&lt;/div&gt;
    &lt;div class="background"&gt;&lt;/div&gt;
    &lt;div class="correctImage"&gt;&lt;/div&gt;
    &lt;canvas class="drawing-main"&gt;&lt;/canvas&gt;
    &lt;canvas class="drawing"&gt;&lt;/canvas&gt;
&lt;/div&gt;
</preview><presenter>function AddonShape_Tracing_create() {

    var NO_POINT = 0;

    function returnErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    function returnCorrectObject(v) { return { isValid: true, value: v }; }

    function getDistance(p1, p2) {
        return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
    }

    Array.prototype.last = Array.prototype.last || function() {
        return this.length === 0 ? null : this[this.length - 1];
    };

    Array.prototype.removeNeighbourDuplicates = Array.prototype.removeNeighbourDuplicates || function() {
        var result = [], last = null;

        for (var i=0; i&lt;this.length; i++) {
            if (this[i] !== last) {
                result.push(this[i]);
                last = this[i];
            }
        }

        return result;
    };

    var presenter = function() {};

    // work-around for double line in android browser
    function setOverflowWorkAround(turnOn) {

        if (!MobileUtils.isAndroidWebBrowser(window.navigator.userAgent)) { return false; }

        var android_ver = MobileUtils.getAndroidVersion(window.navigator.userAgent);
        if (["4.1.1", "4.1.2", "4.2.2", "4.3", "4.4.2"].indexOf(android_ver) !== -1) {

            presenter.$view.parents("*").each(function() {
                var overflow = null;
                if (turnOn) {
                    $(this).attr("data-overflow", $(this).css("overflow"));
                    $(this).css("overflow", "visible");
                } else {
                    overflow = $(this).attr("data-overflow");
                    if (overflow !== "") {
                        $(this).css("overflow", overflow);
                    }
                    $(this).removeAttr("data-overflow");
                }
            });

        }

        return true;
    }

    presenter.data = {
        divID: "",
        width: 0,
        height: 0,
        zoom: 1,

        startColor: "black",
        borderPositions: [],
        activePointsPositions: [],
        isPencilActive: true,
        isStarted: false,
        isAllPointsChecked: false,
        currentPointNumber: 1,
        numberOfLines: 0,
        numberOfDescentsFromShape: 0,
        isShowErrorOn: false,
        incorrect: false,
        isAllOk: false,
        drawingOpacity: 1,

        pencilThickness: 0,

        shapeImageLoaded: null,
        shapeImageLoadedDeferred: null
    };

    var canvasData = {
        main: { canvas: null, context: null },
        temp: { canvas: null, context: null }
    };

    var points = [];

    presenter.cursorPosition = {
        pre_x: 0,
        pre_y: 0,
        x: 0,
        y: 0
    };

    var LINE_END_SIGN = 'Up';
    var DOT_SIGN = 'Dot';

    var eventBus;
    var isOutsideShape = false;
    var directionPoints = [];

    presenter.pointsArray = [];
    presenter.pointsHistory = [];

    // direction from p1 to p2
    function calculateDrawingDirection(p1, p2) {
        var deltaY = p1.y - p2.y;
        var deltaX = p1.x - p2.x;
        var angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
        /*
            ANGLES:
            N 90
            W 0
            E (180, -180)
            S (-90)
         */
        if (angle &gt;= 112 &amp;&amp; angle &lt; 157) {
            return 'NE';
        } else if (angle &gt;= 67 &amp;&amp; angle &lt; 112) {
            return 'N';
        } else if (angle &gt;= 22 &amp;&amp; angle &lt; 67) {
            return 'NW';
        } else if (angle &gt;= -22 &amp;&amp; angle &lt; 22) {
            return 'W';
        } else if (angle &gt;= -67 &amp;&amp; angle &lt; -22) {
            return 'SW';
        } else if (angle &gt;= -112 &amp;&amp; angle &lt; -67) {
            return 'S';
        } else if (angle &gt;= -157 &amp;&amp; angle &lt; -112) {
            return 'SE';
        } else {
            return 'E';
        }

        return false;
    }

    function initPointsArray() {
        for (var i=0; i&lt;presenter.configuration.points.length; i++) {
            presenter.pointsArray.push(i + 1);
        }
    }

    function initCanvasData() {
        canvasData.main.canvas = presenter.$view.find('.drawing-main')[0];
        canvasData.main.context = canvasData.main.canvas.getContext('2d');
        canvasData.temp.canvas = presenter.$view.find('.drawing')[0];
        canvasData.temp.context = canvasData.temp.canvas.getContext('2d');
    }

    function resetCanvas() {
        presenter._turnOffEventListeners();
        $(canvasData.main.canvas).remove();
        $(canvasData.temp.canvas).remove();

        var $main = $('&lt;canvas&gt;&lt;/canvas&gt;').addClass('drawing-main');
        var $temp = $('&lt;canvas&gt;&lt;/canvas&gt;').addClass('drawing');

        presenter.$view.find('.shape-tracing-wrapper').append($main);
        presenter.$view.find('.shape-tracing-wrapper').append($temp);

        initCanvasData();
        resizeCanvas(canvasData.main.canvas);
        resizeCanvas(canvasData.temp.canvas);
        presenter._turnOnEventListeners();
    }

    function resetAddon(isPencilActive) {
        presenter._turnOffEventListeners();
        presenter._turnOnEventListeners();

        canvasData.temp.context.clearRect(0, 0, presenter.data.width, presenter.data.height);
        canvasData.main.context.clearRect(0, 0, presenter.data.width, presenter.data.height);

        presenter.data.isAllPointsChecked = presenter.configuration.points.length === 0;
        presenter.data.isPencilActive = isPencilActive;
        presenter.data.isStarted = false;
        presenter.data.currentPointNumber = 1;
        presenter.data.numberOfLines = 0;
        presenter.data.numberOfDescentsFromShape = 0;
        presenter.data.pencilThickness = presenter.configuration.penThickness;
        presenter.data.incorrect = false;
        presenter.data.isAllOk = false;
        directionPoints = [];
        presenter.pointsHistory = [];
        isOutsideShape = false;
        points = [];

        initPointsArray();

        presenter.configuration.color = presenter.data.startColor;

        if (!presenter.configuration.isShowShapeImage &amp;&amp; presenter.configuration.isShowShapeImageOnCheck) {
            presenter.layer.hide();
        }

        $(canvasData.temp.canvas).removeClass("correct wrong");
        presenter.setVisibility(presenter.visibleByDefault);
        presenter.configuration.isVisible = presenter.visibleByDefault;

        resetCanvas();

        setOverflowWorkAround(true);
        setOverflowWorkAround(false);
    }

    function createEventObject(_item, _value, _score) {
        return {
            'source': presenter.configuration.ID,
            'item': '' + _item,
            'value': '' + _value,
            'score': '' + _score
        };
    }

    function eventCreator() {
        function sendEventScore(isCorrect) {
            presenter.data.isAllOk = isCorrect;
            eventBus.sendEvent('ValueChanged', createEventObject('allOk', '', isCorrect ? '1' : '0'));
        }

        function sendEventValue(isCorrect) {
            presenter.data.isAllOk = isCorrect;
            eventBus.sendEvent('ValueChanged', createEventObject('', isCorrect ? '1' : '0', ''));
        }

        var pointsLength = presenter.configuration.points.length;
        var correctNumOfLines = presenter.configuration.numberOfLines;
        var descentsFromShape = presenter.data.numberOfDescentsFromShape;
        var isAllPointsChecked = presenter.data.isAllPointsChecked;

        function isCorrectNumberOfLines() {
            var numOfDrawLines = presenter.data.numberOfLines;
            return numOfDrawLines &gt;= correctNumOfLines[0] &amp;&amp; correctNumOfLines[1] &lt;= numOfDrawLines;
        }

        if (pointsLength === 0 &amp;&amp; correctNumOfLines.length === 0) {
            if (descentsFromShape === 0) {
                sendEventScore(true);
            } else {
                sendEventValue(false);
            }
        } else if (pointsLength !== 0 &amp;&amp; correctNumOfLines.length === 0) {
            if (descentsFromShape === 0 &amp;&amp; isAllPointsChecked) {
                sendEventScore(true);
            } else {
                sendEventValue(false);
            }
        } else if (pointsLength === 0 &amp;&amp; correctNumOfLines.length !== 0) {
            if (isCorrectNumberOfLines() &amp;&amp; descentsFromShape === 0) {
                sendEventScore(true);
            } else {
                sendEventValue(false);
            }
        } else {
            if (isAllPointsChecked &amp;&amp; !isCorrectNumberOfLines()) {
                // to remember that user was on all points in incorrect number of lines
                presenter.data.incorrect = true;
            }

            if (isCorrectNumberOfLines() &amp;&amp; isAllPointsChecked &amp;&amp; descentsFromShape === 0 &amp;&amp; !presenter.data.incorrect) {
                sendEventScore(true);
            } else {
                sendEventValue(false);
            }
        }
    }

    presenter.initActivePointsPositions = function() {
        // init 2D array filled with zeros
        for (var col=0; col&lt;presenter.data.height; col++) {
            var row = [];
            for (var ro=0; ro&lt;presenter.data.width; ro++) {
                row.push(0);
            }
            presenter.data.activePointsPositions.push(row);
        }

        // mark o=points on array
        for (var pointNumber=0; pointNumber&lt;presenter.configuration.points.length; pointNumber++) {
            var x = presenter.configuration.points[pointNumber][0];
            var y = presenter.configuration.points[pointNumber][1];
            var r = presenter.configuration.points[pointNumber][2];

            for (var i=y-r; i&lt;=y+r; i++) {
                for (var j=x-r; j&lt;=x+r; j++) {
                    if (i &gt;= 0 &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; presenter.data.width &amp;&amp; i &lt; presenter.data.height) {
                        if (r * r &gt;= (x-j) * (x-j) + (y-i) * (y-i)) {
                            presenter.data.activePointsPositions[i][j] = pointNumber + 1;
                        }
                    }
                }
            }
        }
    };

    function calculateBorderCoordinates() {
        function isBoundaryColor(r, g, b, a) {
            return (r &lt; 250 &amp;&amp; g &lt; 250 &amp;&amp; b &lt; 250) &amp;&amp; a === 255;
//            return (r !== 255 || g !== 255 || b !== 255) &amp;&amp; a === 255;
        }

        //var ctx = presenter.layer.getContext('2d');
        var ctx = presenter.layer.getCanvas().getContext();
        var pt = ctx.getImageData(0, 0, presenter.data.width, presenter.data.height);
        var data = pt.data;
        var row;

        for (var y=0; y&lt;presenter.data.height; y++) {
            row = [];
            for (var x=0; x&lt;presenter.data.width; x++) {
                var index = 4 * ((presenter.data.width * y) + x);
                row.push(isBoundaryColor(data[index+0], data[index+1], data[index+2], data[index+3]));
            }
            presenter.data.borderPositions.push(row);
        }
    }

    function showFoundBoundaryPoints() {
        var ctx = presenter.layerBG.getContext('2d');
        var imgData = ctx.getImageData(0, 0, presenter.data.width, presenter.data.height);

        for (var i=0; i&lt;presenter.data.height; i++) {
            for (var j=0; j&lt;presenter.data.width; j++) {
                if (presenter.data.borderPositions[i][j]) {
                    var index = 4 * (i * presenter.data.width + j);
                    imgData.data[index + 0] = 255;
                    imgData.data[index + 1] = 0;
                    imgData.data[index + 2] = 0;
                    imgData.data[index + 3] = 255;
                }
            }
        }

        ctx.putImageData(imgData, 0, 0, presenter.data.width, presenter.data.height);
    }

    function colorNameToHex(color) {
        var colors = {"aliceblue":"#f0f8ff","antiquewhite":"#faebd7","aqua":"#00ffff","aquamarine":"#7fffd4","azure":"#f0ffff","beige":"#f5f5dc","bisque":"#ffe4c4","black":"#000000","blanchedalmond":"#ffebcd","blue":"#0000ff","blueviolet":"#8a2be2","brown":"#a52a2a","burlywood":"#deb887","cadetblue":"#5f9ea0","chartreuse":"#7fff00","chocolate":"#d2691e","coral":"#ff7f50","cornflowerblue":"#6495ed","cornsilk":"#fff8dc","crimson":"#dc143c","cyan":"#00ffff","darkblue":"#00008b","darkcyan":"#008b8b","darkgoldenrod":"#b8860b","darkgray":"#a9a9a9","darkgreen":"#006400","darkkhaki":"#bdb76b","darkmagenta":"#8b008b","darkolivegreen":"#556b2f","darkorange":"#ff8c00","darkorchid":"#9932cc","darkred":"#8b0000","darksalmon":"#e9967a","darkseagreen":"#8fbc8f","darkslateblue":"#483d8b","darkslategray":"#2f4f4f","darkturquoise":"#00ced1","darkviolet":"#9400d3","deeppink":"#ff1493","deepskyblue":"#00bfff","dimgray":"#696969","dodgerblue":"#1e90ff","firebrick":"#b22222","floralwhite":"#fffaf0","forestgreen":"#228b22","fuchsia":"#ff00ff","gainsboro":"#dcdcdc","ghostwhite":"#f8f8ff","gold":"#ffd700","goldenrod":"#daa520","gray":"#808080","green":"#008000","greenyellow":"#adff2f","honeydew":"#f0fff0","hotpink":"#ff69b4","indianred ":"#cd5c5c","indigo ":"#4b0082","ivory":"#fffff0","khaki":"#f0e68c","lavender":"#e6e6fa","lavenderblush":"#fff0f5","lawngreen":"#7cfc00","lemonchiffon":"#fffacd","lightblue":"#add8e6","lightcoral":"#f08080","lightcyan":"#e0ffff","lightgoldenrodyellow":"#fafad2","lightgrey":"#d3d3d3","lightgreen":"#90ee90","lightpink":"#ffb6c1","lightsalmon":"#ffa07a","lightseagreen":"#20b2aa","lightskyblue":"#87cefa","lightslategray":"#778899","lightsteelblue":"#b0c4de","lightyellow":"#ffffe0","lime":"#00ff00","limegreen":"#32cd32","linen":"#faf0e6","magenta":"#ff00ff","maroon":"#800000","mediumaquamarine":"#66cdaa","mediumblue":"#0000cd","mediumorchid":"#ba55d3","mediumpurple":"#9370d8","mediumseagreen":"#3cb371","mediumslateblue":"#7b68ee","mediumspringgreen":"#00fa9a","mediumturquoise":"#48d1cc","mediumvioletred":"#c71585","midnightblue":"#191970","mintcream":"#f5fffa","mistyrose":"#ffe4e1","moccasin":"#ffe4b5","navajowhite":"#ffdead","navy":"#000080","oldlace":"#fdf5e6","olive":"#808000","olivedrab":"#6b8e23","orange":"#ffa500","orangered":"#ff4500","orchid":"#da70d6","palegoldenrod":"#eee8aa","palegreen":"#98fb98","paleturquoise":"#afeeee","palevioletred":"#d87093","papayawhip":"#ffefd5","peachpuff":"#ffdab9","peru":"#cd853f","pink":"#ffc0cb","plum":"#dda0dd","powderblue":"#b0e0e6","purple":"#800080","red":"#ff0000","rosybrown":"#bc8f8f","royalblue":"#4169e1","saddlebrown":"#8b4513","salmon":"#fa8072","sandybrown":"#f4a460","seagreen":"#2e8b57","seashell":"#fff5ee","sienna":"#a0522d","silver":"#c0c0c0","skyblue":"#87ceeb","slateblue":"#6a5acd","slategray":"#708090","snow":"#fffafa","springgreen":"#00ff7f","steelblue":"#4682b4","tan":"#d2b48c","teal":"#008080","thistle":"#d8bfd8","tomato":"#ff6347","turquoise":"#40e0d0","violet":"#ee82ee","wheat":"#f5deb3","white":"#ffffff","whitesmoke":"#f5f5f5","yellow":"#ffff00","yellowgreen":"#9acd32"};
        var parsedColor = Array.isArray(color) ? color[0] : color;

        if (typeof colors[parsedColor.toLowerCase()] !== 'undefined') {
            return colors[parsedColor.toLowerCase()];
        } else {
            return false;
        }
    }

    function drawActivePoints() {
        for (var i=0; i&lt;presenter.configuration.points.length; i++) {
            var point = new Kinetic.Circle({
                x: presenter.configuration.points[i][0],
                y: presenter.configuration.points[i][1],
                radius: presenter.configuration.points[i][2],
                fill: 'rgba(0,255,0,0.25)',
                stroke: 'green',
                strokeWidth: 2
            });
            presenter.layerBG.add(point);
        }

        presenter.stageBG.add(presenter.layerBG);
    }

    function drawBoxPointerData(box_width, box_height) {
        var position = -4;
        presenter.box = new Kinetic.Rect({
            x: position, y: position,
            stroke: '#555',
            fill: '#ddd',
            width: box_width,
            height: box_height,
            cornerRadius: 7,
            opacity: 0.6
        });
        presenter.text = new Kinetic.Text({
            x: position, y: position,
            text: prepearText(0, 0),
            fontSize: 15,
            fontFamily: 'Calibri',
            fill: '#555',
            width: box_width,
            padding: 4,
            align: 'center'
        });

        presenter.layerBG.add(presenter.box);
        presenter.layerBG.add(presenter.text);
        presenter.stageBG.add(presenter.layerBG);
    }

    function prepearText(x, y) {
        function addZerosToNumber(n) {
            switch (n.toString().length) {
                case 1: return "000" + n;
                case 2: return "00" + n;
                case 3: return "0" + n;
                default: return "" + n;
            }
        }

        return "X:" + addZerosToNumber(x) + "\nY:" + addZerosToNumber(y);
    }

    function cursorCoordinates() {
        drawBoxPointerData(52, 37);

        presenter.data.shapeImageLoaded.then(function() {
            const moduleSelector = $(`.moduleSelector[data-id="${presenter.configuration.ID}"]`)[0];
            moduleSelector.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, function(event) {
                if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                    return;
                }
                if (EventsUtils.PointingEvents.hasPointerEventSupport()) {
                    moduleSelector.releasePointerCapture(event.pointerId);
                }

                presenter.text.setText(prepearText(event.offsetX, event.offsetY));
                presenter.layerBG.draw();
            }, false);
        });

        const drawingElement = presenter.view.getElementsByClassName("drawing")[0];
        drawingElement.addEventListener(EventsUtils.PointingEvents.TYPES.MOVE, function(event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            if (EventsUtils.PointingEvents.hasPointerEventSupport()) {
                drawingElement.releasePointerCapture(event.pointerId);
            }
            event.stopPropagation();

            presenter.text.setText(prepearText(event.offsetX, event.offsetY));
            presenter.layerBG.draw();
        }, false);
    }

    function drawBackGroundImage(isPreview) {
        presenter.stageBG = new Kinetic.Stage({
            container: presenter.data.divID + "_background",
            height: presenter.data.height,
            width: presenter.data.width
        });

        presenter.layerBG = new Kinetic.Layer();
        if (presenter.configuration.backgroundImage !== '') {
            var backgroundImage = new Image();
            backgroundImage.onload = function() {
                var BGimg = new Kinetic.Image({
                    x: 0,
                    y: 0,
                    height: presenter.data.height,
                    width: presenter.data.width,
                    image: backgroundImage
                });

                presenter.layerBG.add(BGimg);
                presenter.stageBG.add(presenter.layerBG);

                if (isPreview) {
                    cursorCoordinates();
                    drawActivePoints();
                }
            };

            URLUtils.prepareImageForCanvas(presenter.playerController, backgroundImage, presenter.configuration.backgroundImage);
        } else {
            if (isPreview) {
                cursorCoordinates();
                drawActivePoints();
            }
        }
    }

    function drawShapeImage(isPreview) {
        presenter.stage = new Kinetic.Stage({
            container: presenter.data.divID + "_shape",
            height: presenter.data.height,
            width: presenter.data.width
        });
        presenter.layer = new Kinetic.Layer();
        var image = new Image();
        image.onload = function() {
            var img = new Kinetic.Image({
                x: 0,
                y: 0,
                image: image,
                height: presenter.data.height,
                width: presenter.data.width
            });

            presenter.layer.add(img);
            presenter.stage.add(presenter.layer);

            calculateBorderCoordinates();

            if (!presenter.configuration.isShowShapeImage) {
                presenter.layer.hide();
            }

            if (isPreview) {
                if (presenter.configuration.isShowFoundBoundaries) {
                    showFoundBoundaryPoints();
                }
            }
            presenter.data.shapeImageLoadedDeferred.resolve();
        };

        URLUtils.prepareImageForCanvas(presenter.playerController, image, presenter.configuration.shapeImage);
    }

    function drawCorrectAnswerImage(isPreview) {
        presenter.stageCorrect = new Kinetic.Stage({
            container: presenter.data.divID + "_correctImage",
            height: presenter.data.height,
            width: presenter.data.width
        });
        presenter.correctAnswerlayer = new Kinetic.Layer();
        var correctImage = new Image();
        correctImage.onload = function() {
            var correctImg = new Kinetic.Image({
                x: 0, y: 0,
                image: correctImage,
                height: presenter.data.height,
                width: presenter.data.width
            });

            presenter.correctAnswerlayer.add(correctImg);
            presenter.stageCorrect.add(presenter.correctAnswerlayer);

            calculateBorderCoordinates();
        };

        URLUtils.prepareImageForCanvas(presenter.playerController, correctImage, presenter.configuration.correctAnswerImage);
    }

    function updateCursorPosition(event) {
        presenter.cursorPosition.pre_x = presenter.cursorPosition.x;
        presenter.cursorPosition.pre_y = presenter.cursorPosition.y;

        var canvas = canvasData.temp.canvas;
        var rect = canvas.getBoundingClientRect();

        const clientX = event.originalEvent ? event.originalEvent.clientX : event.clientX;
        if (clientX !== undefined) {
            const clientY = event.originalEvent ? event.originalEvent.clientY : event.clientY;
            presenter.cursorPosition.x = parseInt((clientX - rect.left) / presenter.data.zoom, 10);
            presenter.cursorPosition.y = parseInt((clientY - rect.top) / presenter.data.zoom, 10);
        } else {
            const targetTouches = event.originalEvent ? event.originalEvent.targetTouches : event.targetTouches;
            presenter.cursorPosition.x = parseInt((targetTouches[0].pageX - $(canvas).offset().left) / presenter.data.zoom, 10);
            presenter.cursorPosition.y = parseInt((targetTouches[0].pageY - $(canvas).offset().top) / presenter.data.zoom, 10);
        }

        directionPoints.push({ x: presenter.cursorPosition.x, y: presenter.cursorPosition.y });
    }

    function upDateCheckPointsHistory(x, y) {
        var point = presenter.data.activePointsPositions[y][x];
        var lastPointInArray = presenter.pointsHistory.last();

        if (point !== lastPointInArray) {
            presenter.pointsHistory.push(point);
        }
    }

    presenter.isShapeCoveredInCircle = function(x, y, r) {
        r = parseInt(r, 10);
        var increase = r &lt; 4 ? r : 4;

        for (var i=y-r; i&lt;=y+r; i += increase) {
            for (var j=x-r; j&lt;=x+r; j += increase) {
                if (i &gt; 0 &amp;&amp; j &gt; 0 &amp;&amp; j &lt; presenter.data.width &amp;&amp; i &lt; presenter.data.height) {
                    if (r * r &gt;= (x-j) * (x-j) + (y-i) * (y-i)) {
                        if (!presenter.data.borderPositions[i][j]) return false;
                    }
                }
            }
        }

        return true;
    };

    presenter.isThisActivePointAndCheck = function(x, y) {
        // check if point is in array
        var result = presenter.pointsArray.indexOf(presenter.data.activePointsPositions[x][y]) !== -1;

        // remove point from array
        presenter.pointsArray = presenter.pointsArray.filter(function(n) { return n !== presenter.data.activePointsPositions[x][y] });

        return result;
    };

    presenter.isPositionInDefinedPoint = function(x, y, r) {
        r = parseInt(r, 10);

        for (var i=y-r; i&lt;=y+r; i += 3) {
            for (var j=x-r; j&lt;=x+r; j += 3) {
                if (i &gt; 0 &amp;&amp; j &gt; 0 &amp;&amp; j &lt; presenter.data.width &amp;&amp; i &lt; presenter.data.height) {
                    if (r * r &lt;= (x-j) * (x-j) + (y-i) * (y-i)) {
                        upDateCheckPointsHistory(x, y);

                        // if points' order does matter
                        if (presenter.configuration.isCheckPointsOrder) {
                            if (presenter.data.activePointsPositions[i][j] === presenter.data.currentPointNumber) {
                                return true;
                            }
                        // if not then find any point
                        } else {
                            if (presenter.isThisActivePointAndCheck(i, j)) {
                                return true;
                            }
                        }
                    }
                }
            }
        }

        return false;
    };

    function checkCorrectness() {
        const x = parseInt(presenter.cursorPosition.x, 10);
        const y = parseInt(presenter.cursorPosition.y, 10);
        const point = scalePoint({x: x, y: y});
        if (presenter.isShapeCoveredInCircle(point.x, point.y, presenter.data.pencilThickness / 2)) {
            isOutsideShape = false;
        } else {
            if (!isOutsideShape) {
                presenter.data.numberOfDescentsFromShape++;
                isOutsideShape = true;
            }
        }

        if (presenter.isPositionInDefinedPoint(point.x, point.y, presenter.data.pencilThickness / 2)) {
            presenter.data.currentPointNumber++;
            if (presenter.data.currentPointNumber &gt; presenter.configuration.points.length) {
                presenter.data.isAllPointsChecked = true;
            }
        }
    }

    function scalePoint({x, y}) {
        const scaledPoint = {x: x, y: y};
        if (!presenter.playerController) {
            return scaledPoint;
        }

        const scale = presenter.playerController.getScaleInformation();
        if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
            scaledPoint.x = Math.floor(scaledPoint.x / scale.scaleX);
            scaledPoint.y = Math.floor(scaledPoint.y / scale.scaleY);
        }
        return scaledPoint;
    }

    function draw(e, notPropagate) {
        if (notPropagate) {
            e.stopPropagation();
            e.preventDefault();
        }

        updateCursorPosition(e);

        var ctx = canvasData.temp.context;
        var can = canvasData.temp.canvas;

        ctx.globalAlpha = presenter.data.drawingOpacity;
        ctx.lineWidth = presenter.data.pencilThickness;
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.strokeStyle = presenter.configuration.color;
        ctx.fillStyle = presenter.configuration.color;


        var point = {x: presenter.cursorPosition.x, y: presenter.cursorPosition.y};
        var scale = presenter.playerController.getScaleInformation();
        if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
            point.x = point.x / scale.scaleX;
            point.y = point.y / scale.scaleY;
        }
        points.push(point);

        if (points.length &lt; 3) {
            ctx.beginPath();
            ctx.arc(points[0].x, points[0].y, presenter.data.pencilThickness / 2, 0, Math.PI * 2, !0);
            ctx.fill();
            ctx.closePath();
        } else {
            ctx.clearRect(0, 0, can.width, can.height);

            ctx.beginPath();
            ctx.moveTo(points[0].x, points[0].y);

            for (var i=1; i&lt;points.length-2; i++) {
                var x = (points[i].x + points[i + 1].x) / 2;
                var y = (points[i].y + points[i + 1].y) / 2;

                ctx.quadraticCurveTo(points[i].x, points[i].y, x, y);
            }

            ctx.quadraticCurveTo(
                points[i].x, points[i].y,
                points[i + 1].x, points[i + 1].y
            );
            ctx.stroke();
        }

        if (presenter.data.isPencilActive) {
            // active only on drawing, disable when eraser
            checkCorrectness();
        }
    }

    presenter._turnOnEventListeners = function () {
        const $canvas = $(canvasData.temp.canvas);
        $canvas.on('click', function(e) {
            e.stopPropagation();
        });

        if (EventsUtils.PointingEvents.hasPointerEventSupport() || !MobileUtils.isEventSupported('touchstart')) {
            presenter._connectPointerEvents($canvas);
        } else {
            presenter._connectTouchEvents($canvas);
        }
    };

    presenter._connectTouchEvents = function ($canvas) {
        var isWorkaroundOn = false;

        $canvas.on('touchstart', function(e) {
            presenter.data.numberOfLines++;

            if (presenter.data.isPencilActive) {
                presenter.data.isStarted = true;
                setOverflowWorkAround(true);
                draw(e, false);
            } else {
                resetAddon(false);
            }
        });

        $canvas.on('touchmove', function(e) {
            if (presenter.data.isPencilActive) {
                presenter.data.isStarted = true;
                if (!isWorkaroundOn) {
                    setOverflowWorkAround(true);
                }
                drawWithoutPropagation(e);
            } else {
                resetAddon(false);
            }

        });

        $canvas.on('touchend', function() {
            if (presenter.data.isPencilActive) {
                eventCreator();
            }

            canvasData.main.context.drawImage(canvasData.temp.canvas, 0, 0);
            canvasData.temp.context.clearRect(0, 0, canvasData.temp.canvas.width, canvasData.temp.canvas.height);

            points = [];
            directionPoints.push('Up');

            setOverflowWorkAround(false);
            isWorkaroundOn = false;
        });
    };
    
    function drawWithoutPropagation (e) {
        draw(e, true);
    }

    presenter._connectPointerEvents = function ($canvas) {
        var isDown = false;

        $canvas.on(EventsUtils.PointingEvents.TYPES.DOWN, function(event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            isDown = true;
            draw(event, false);
            $canvas.on(EventsUtils.PointingEvents.TYPES.MOVE, drawWithoutPropagation);

            if (presenter.data.isPencilActive) {
                presenter.data.isStarted = true;
                presenter.data.numberOfLines++;
            } else {
                resetAddon(false);
            }
        });

        $canvas.on(`${EventsUtils.PointingEvents.TYPES.UP} ${EventsUtils.PointingEvents.TYPES.LEAVE}`, function(event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            $canvas.off(EventsUtils.PointingEvents.TYPES.MOVE, drawWithoutPropagation);
            if (isDown &amp;&amp; presenter.data.isPencilActive) {
                eventCreator();
                isDown = false;
            }

            canvasData.main.context.drawImage(canvasData.temp.canvas, 0, 0);
            canvasData.temp.context.clearRect(0, 0, canvasData.temp.canvas.width, canvasData.temp.canvas.height);

            points = [];
        });

        $canvas.on(EventsUtils.PointingEvents.TYPES.UP, function(event) {
            if (!EventsUtils.PointingEvents.isPrimaryEvent(event)) {
                return;
            }
            directionPoints.push('Up');
        });
    };

    presenter._turnOffEventListeners = function () {
        const $canvas = $(canvasData.temp.canvas);
        $canvas.off("touchstart touchend touchmove");
        $canvas.off(`${EventsUtils.PointingEvents.TYPES.DOWN} ${EventsUtils.PointingEvents.TYPES.UP} ${EventsUtils.PointingEvents.TYPES.MOVE} ${EventsUtils.PointingEvents.TYPES.LEAVE}`);
    };

    presenter.ERROR_CODES = {
        SI01: "Property Shape image cannot be empty",

        P02: "Wrong amount of numbers in every line in Points' coordinates property",
        P03: "Points' coordinates are out of canvas range",
        P04: "Non numeric value in points' coordinates property",

        C01: "Wrong value in property: Color",

        T02: "Property Thickness cannot be less then 1 and more then 40",

        O01: "Property Opacity has to be between 0 and 1",

        B01: "Property Border hat to be between 0 and 5"
    };

    function parseImage(img) {
        if (ModelValidationUtils.isStringWithPrefixEmpty(img, "/file/")) {
            return returnErrorObject("SI01");
        }

        return returnCorrectObject(img);
    }

    function parseBGImage(BGImage) {
        return returnCorrectObject(BGImage);
    }

    function parseNumberOfLines(lines) {
        if (ModelValidationUtils.isStringEmpty(lines)) {
            return returnCorrectObject([]);
        }

        lines = lines.split(';').map(function (num) { return parseInt(num, 10); });
        lines = lines.length === 1 ? lines.concat(lines) : lines;

        return returnCorrectObject(lines);
    }

    function parsePoints(points) {
        if (ModelValidationUtils.isStringEmpty(points)) {
            return returnCorrectObject([]);
        }

        // e.g. "1;1;1\n2;2;2\n3;3;3" =&gt; [[1,1,1], [2,2,2], [3,3,3]]
        points = Helpers.splitLines(points).map(function(line) { return line.split(';').map(function(num){ return parseInt(num, 10) }) });

        for (var i=0, len=points.length; i&lt;len; i++) {
            if (points[i].length !== 3) {
                return returnErrorObject("P02");
            }

            if (points[i][0] &lt; 0 || points[i][1] &lt; 0 || points[i][0] &gt; presenter.data.width || points[i][1] &gt; presenter.data.height) {
                return returnErrorObject("P03");
            }
        }

        return returnCorrectObject(points);
    }

    function parseColor(color) {
        color = color.trim();
        if (ModelValidationUtils.isStringEmpty(color)) {
            return returnCorrectObject('#000000');
        }

        if (color[0] === '#' &amp;&amp; !(color.length === 7 || color.length === 4)) {
            return returnErrorObject("C01");
        }

        if (color[0] !== '#') {
            color = colorNameToHex(color);
            if (!color) return returnErrorObject("C01");
        }

        return returnCorrectObject(color);
    }

    function parseThickness(thickness) {
        if (ModelValidationUtils.isStringEmpty(thickness)) {
            return returnCorrectObject(10);
        }

        thickness = parseInt(thickness, 10);

        if (1 &gt; thickness || thickness &gt; 40) {
            return returnErrorObject("T02");
        }

        return returnCorrectObject(thickness);
    }

    function parseOpacity(opacity) {
        opacity = opacity || 1;
        opacity = parseFloat(opacity);

        if (0 &gt; opacity || opacity &gt; 1) {
            return returnErrorObject("O01");
        }

        return returnCorrectObject(opacity);
    }

    function parseBorder(border) {
        border = border || 0;
        border = parseInt(border, 10);

        if (0 &gt; border || border &gt; 5) {
            return returnErrorObject("B01");
        }

        return returnCorrectObject(border);
    }

    presenter.validateModel = function(model) {
        var validatedShapeImage = parseImage(model["Shape image"]);
        if (!validatedShapeImage.isValid) {
            return returnErrorObject(validatedShapeImage.errorCode);
        }

        var validatedBGImage = parseBGImage(model["Background image"]);
        if (!validatedBGImage.isValid) {
            return returnErrorObject(validatedBGImage.errorCode);
        }

        var validatedCorrectNumberOfLines = parseNumberOfLines(model["Correct number of lines"]);
        if (!validatedCorrectNumberOfLines.isValid) {
            return returnErrorObject(validatedCorrectNumberOfLines.errorCode);
        }

        var validatedPoints = parsePoints(model["Points' coordinates"]);
        if (!validatedPoints.isValid) {
            return returnErrorObject(validatedPoints.errorCode);
        }

        var validatedColor = parseColor(model["Color"]);
        if (!validatedColor.isValid) {
            return returnErrorObject(validatedColor.errorCode);
        }

        presenter.data.startColor = validatedColor.value;

        var validatedThickness_Pen = parseThickness(model["Pen Thickness"]);
        if (!validatedThickness_Pen.isValid) {
            return returnErrorObject(validatedThickness_Pen.errorCode);
        }

        var validatedOpacity = parseOpacity(model["Opacity"]);
        if (!validatedOpacity.isValid) {
            return returnErrorObject(validatedOpacity.errorCode);
        }

        var validatedBorder = parseBorder(model["Border"]);
        if (!validatedBorder.isValid) {
            return returnErrorObject(validatedBorder.errorCode);
        }

        return {
            shapeImage: validatedShapeImage.value,
            isShowShapeImage: ModelValidationUtils.validateBoolean(model["Show Shape image"]),
            isShowShapeImageOnCheck: !ModelValidationUtils.validateBoolean(model["Hide Shape image on check"]),
            isShowFoundBoundaries: ModelValidationUtils.validateBoolean(model["Show Boundaries (editor)"]),
            backgroundImage: validatedBGImage.value,
            numberOfLines: validatedCorrectNumberOfLines.value,
            points: validatedPoints.value,
            isCheckPointsOrder: ModelValidationUtils.validateBoolean(model["isPointsOrder"]),
            color: validatedColor.value,
            penThickness: validatedThickness_Pen.value,
            opacity: validatedOpacity.value,
            border: validatedBorder.value,
            correctAnswerImage: model["Correct Answer Image"],
            numberOfPoints: validatedPoints.value.length,

            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isValid: true
        }
    };

    function resizeCanvas(elem) {
        var container = $(elem).parent();

        elem.width = container.width();
        elem.height = container.height();
    }

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.data.shapeImageLoadedDeferred = new $.Deferred();
        presenter.data.shapeImageLoaded = presenter.data.shapeImageLoadedDeferred.promise();

        presenter.view = view;
        presenter.$view = $(view);

        Kinetic.pixelRatio = 1;
        var zoom = $('#_icplayer').css('zoom');
        presenter.data.zoom = zoom == "" || zoom == undefined || isNaN(zoom) ? 1 : zoom;
        presenter.data.width = parseInt(model["Width"], 10);
        presenter.data.height = parseInt(model["Height"], 10);

        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        presenter.initializeCanvas(isPreview);

        if (!isPreview) {
            presenter._turnOnEventListeners();
        }

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);
        presenter.visibleByDefault = presenter.configuration.isVisible;

        presenter.$view.find('div.correctImage').css('display', 'none');

        return false;
    };

    presenter.initializeCanvas = function(isPreview) {
        initPointsArray();

        presenter.data.isAllPointsChecked = presenter.configuration.points.length === 0;
        presenter.data.pencilThickness = presenter.configuration.penThickness;

        presenter.initActivePointsPositions();

        if (presenter.configuration.border !== 0) {
            presenter.$view.css('border', presenter.configuration.border + 'px solid black');
        }

        presenter.data.divID = presenter.configuration.ID + (isPreview ? "_preview" : "_run");

        presenter.$view.find("div.background").attr('id', presenter.data.divID + "_background");
        presenter.$view.find("div.shape").attr('id', presenter.data.divID + "_shape");
        presenter.$view.find("div.correctImage").attr('id', presenter.data.divID + "_correctImage");

        presenter.$view.css('opacity', presenter.configuration.opacity);

        initCanvasData();

        resizeCanvas(canvasData.main.canvas);
        resizeCanvas(canvasData.temp.canvas);

        drawBackGroundImage(isPreview);
        drawShapeImage(isPreview);

        presenter.data.shapeImageLoaded.then(function() {
            if (presenter.configuration.correctAnswerImage &amp;&amp; !isPreview) {
                drawCorrectAnswerImage(isPreview);
            }
        });
    };

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model, false);

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        eventBus = controller.getEventBus();
    };

    presenter.setThickness = function(thickness) {
        presenter.data.pencilThickness = parseThickness(thickness).value;
    };

    presenter.setColor = function(color) {
        presenter.data.isPencilActive = true;
        presenter.configuration.color = parseColor(color).value;
    };

    presenter.setEraserOn = function() {
        presenter.data.isPencilActive = false;
    };

    presenter.setEraserOff = function() {
        presenter.data.isPencilActive = true;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setOpacity = function(opacity) {
        presenter.data.drawingOpacity = parseOpacity(opacity[0]).value;
    };

    presenter.descentsFromShape = function() {
        return presenter.data.numberOfDescentsFromShape;
    };

    presenter.numberOfLines = function() {
        return presenter.data.numberOfLines;
    };

    presenter.pointsMissed = function() {
        var points = presenter.pointsHistory.filter(function(p) { return p !== NO_POINT });
        var result = [];

        for (var i=1; i&lt;presenter.configuration.numberOfPoints+1; i++) {
            if (points.indexOf(i) === -1) {
                result.push(i);
            }
        }

        return result.length;
    };

    function parseLinesToDots(points, distance) {
        function getLineFromIndex(points, index) {
            var result = [];

            for (var i=index; i&lt;points.length; i++) {
                if (points[i] === LINE_END_SIGN) {
                    return result;
                } else {
                    result.push(points[i]);
                }
            }

            return result;
        }

        var result = [], i = 0;

        while (i &lt; points.length) {
            var line = getLineFromIndex(points, i);

            if (line.length === 1 || getDistance(line[0], line.last()) &lt; distance) {
                result.push(DOT_SIGN);
            } else {
                result = result.concat(line);
            }

            result.push(LINE_END_SIGN);

            i += line.length + 1;
        }

        return result;
    }

    presenter.getDirections = function() {
        function hasUndefinedValue() {
            for (var i=0; i&lt;arguments.length; i++) {
                if (arguments[i] === undefined) {
                    return true;
                }
            }

            return false;
        }

        if (directionPoints.length === 0) {
            return [];
        }

        var MIN_DISTANCE = 8;
        directionPoints = parseLinesToDots(directionPoints.removeNeighbourDuplicates(), MIN_DISTANCE);

        var result = [], i = 0,
            p1 = directionPoints[0],
            p2 = directionPoints[1];

        while (i &lt; directionPoints.length-1) {
            while (p1 === LINE_END_SIGN || p1 === DOT_SIGN) {
                result.push(p1);
                i++;
                p1 = directionPoints[i];
                p2 = directionPoints[i + 1];
            }

            if (hasUndefinedValue(p1, p2)) {
                break;
            }

            if (getDistance(p1, p2) &gt; MIN_DISTANCE) {
                result.push(calculateDrawingDirection(p1, p2));
                p1 = directionPoints[i + 1];
            }

            p2 = directionPoints[i + 2];

            while (p2 === LINE_END_SIGN || p2 === DOT_SIGN) {
                result.push(p2);
                i++;
                p1 = directionPoints[i];
                p2 = directionPoints[i + 1];
            }

            i++;
        }

        return result.removeNeighbourDuplicates();
    };

    function getDrawnUniqueValues() {
        var uniqueValues = [];
        var points = presenter.pointsHistory.filter(function(p) { return p !== NO_POINT });

        for (var i = 0; i &lt; points.length; i++) {
            if (uniqueValues.indexOf(points[i]) === -1) {
                uniqueValues.push(points[i]);
            }
        }

        return uniqueValues;
    }

    presenter.isOrderCorrect = function(skipPoints) {
        skipPoints = skipPoints === 'true' || skipPoints === true;

        var i, previous = -1;
        var drawnPoints = getDrawnUniqueValues();
        if (skipPoints) {
            for (i = 0; i &lt; drawnPoints.length; i++) {
                if (previous &gt; drawnPoints[i]) {
                    return false;
                }
                previous = drawnPoints[i];
            }
        } else {
            for (i = 0; i &lt; drawnPoints.length; i++) {
                if (drawnPoints[i] !== i + 1) {
                    return false;
                }
            }
        }

        return true;
    };

    presenter.isOrderCorrectCommand = function(params) {
        return presenter.isOrderCorrect(params[0]);
    };

    presenter.executeCommand = function(name, params) {
        if (!presenter.configuration.isValid) {
            return;
        }

        var commands = {
            "reset": presenter.reset,
            "show": presenter.show,
            "hide": presenter.hide,
            "setEraserOn": presenter.setEraserOn,
            "setEraserOff": presenter.setEraserOff,
            "setThickness": presenter.setThickness,
            "showAnswers": presenter.showAnswers,
            "hideAnswers": presenter.hideAnswers,
            "setColor": presenter.setColor,
            "setOpacity": presenter.setOpacity,
            "isOrderCorrect": presenter.isOrderCorrectCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    function countScore() {
        var correctLinesNum = presenter.configuration.numberOfLines;
        var linesNum = presenter.data.numberOfLines;
        var numOfDesc = presenter.data.numberOfDescentsFromShape;

        if (correctLinesNum.length === 0) {
            if (numOfDesc === 0 &amp;&amp; presenter.data.isAllPointsChecked) {
                return 1;
            } else {
                return 0;
            }
        } else {
            if (correctLinesNum[0] &lt;= linesNum &amp;&amp; linesNum &lt;= correctLinesNum[1] &amp;&amp; numOfDesc === 0 &amp;&amp; presenter.data.isAllPointsChecked) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter._turnOffEventListeners();

        if (presenter.data.isStarted) {
            $(canvasData.temp.canvas).addClass(countScore() === 1 ? "correct" : "wrong");
            if (!presenter.configuration.isShowShapeImage &amp;&amp; presenter.configuration.isShowShapeImageOnCheck) {
                presenter.layer.show();
            }
        }
    };

    presenter.setWorkMode = function() {
        presenter._turnOffEventListeners();
        presenter._turnOnEventListeners();

        if (!presenter.configuration.isShowShapeImage) {
            presenter.layer.hide();
        }

        $(canvasData.temp.canvas).removeClass("correct wrong");
    };

    presenter.reset = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        resetAddon(true);
    };

    presenter.getErrorCount = function() {
        if (!presenter.data.isStarted) {
            return 0;
        }

        return countScore() === 0 ? 1 : 0;
    };

    presenter.getMaxScore = function() {
        return 1;
    };

    presenter.getScore = function() {
        if (!presenter.data.isStarted) {
            return 0;
        }

        return countScore();
    };

    presenter.getState = function() {
        return JSON.stringify({
            imgData: canvasData.main.canvas.toDataURL("image/png"),
            isPencilActive: presenter.data.isPencilActive,
            color: presenter.configuration.color,
            currentPointNumber: presenter.data.currentPointNumber,
            numberOfLines: presenter.data.numberOfLines,
            numberOfDescentsFromShape: presenter.data.numberOfDescentsFromShape,
            isAllPointsChecked: presenter.data.isAllPointsChecked,
            isAllOk: presenter.data.isAllOk,
            pointsArray: presenter.pointsArray,
            isVisible: presenter.configuration.isVisible,
            directionPoints: directionPoints
        });
    };

    presenter.upgradeStateForVisibility = function(state) {
        if (state.isVisible === undefined) {
            state.isVisible = true;
        }

        return state;
    };

    presenter.upgradeStateForOpacity = function(state) {
        if (state.opacity === undefined) {
            state.opacity = 0.9;
        }

        return state;
    };

    presenter.upgradeStateForDirectionPoints = function(state) {
        if (state.directionPoints === undefined) {
            state.directionPoints = [];
        }

        return state;
    };

    presenter.upgradeState = function (parsedState) {
        parsedState = presenter.upgradeStateForVisibility(parsedState);
        parsedState = presenter.upgradeStateForOpacity(parsedState);
        parsedState = presenter.upgradeStateForDirectionPoints(parsedState);

        return parsedState;
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsedState = presenter.upgradeState(JSON.parse(state));

        presenter.data.isStarted = true; // state is non empty =&gt; exercise is started
        presenter.data.currentPointNumber = parsedState.currentPointNumber;
        presenter.data.numberOfLines = parsedState.numberOfLines;
        presenter.data.numberOfDescentsFromShape = parsedState.numberOfDescentsFromShape;
        presenter.data.isAllPointsChecked = parsedState.isAllPointsChecked;
        presenter.data.isAllOk = parsedState.isAllOk || false;
        presenter.pointsArray = parsedState.pointsArray;
        presenter.configuration.isVisible = parsedState.isVisible;
        directionPoints = parsedState.directionPoints;

        var savedImg = new Image();
        savedImg.onload = function() {
            canvasData.main.context.drawImage(savedImg, 0, 0);

            presenter.configuration.color = JSON.parse(state).color;
            if (!JSON.parse(state).isPencilActive) {
                presenter.setEraserOn();
            }

            setOverflowWorkAround(true);
            setOverflowWorkAround(false);
        };

        savedImg.src = JSON.parse(state).imgData;

        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.isAllOk = function() {
        return presenter.data.isAllOk;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function() {
        presenter.isShowAnswersActive = true;
        presenter.setWorkMode();
        presenter._turnOffEventListeners();

        if (presenter.configuration.correctAnswerImage) {
            presenter.layer.hide();
            presenter.$view.find('.correctImage').css('display', 'block');
        } else {
            presenter.layer.show();
        }

        presenter.$view.find('.background').addClass('shape-tracing-show-answers');
    };

    presenter.hideAnswers = function() {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        presenter.$view.find('.background').removeClass('shape-tracing-show-answers');

        if (presenter.correctAnswerlayer) {
            presenter.$view.find('.correctImage').css('display', 'none');
        }
        if (presenter.configuration.isShowShapeImage) {
            presenter.layer.show();
        } else {
            presenter.layer.hide();
        }
        presenter._turnOffEventListeners();
        presenter._turnOnEventListeners();
        presenter.isShowAnswersActive = false;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Shooting_Range" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Definitions" name="definitions" nameLabel="Shooting_Range_property_definitions" type="list">
            <property displayName="Definition" name="definition" nameLabel="Shooting_Range_property_definition" type="html"/>
            <property displayName="Answer" name="answer1" nameLabel="Shooting_Range_property_answer" type="string"/>
            <property displayName="Answer" name="answer2" nameLabel="Shooting_Range_property_answer" type="string"/>
            <property displayName="Answer" name="answer3" nameLabel="Shooting_Range_property_answer" type="string"/>
            <property displayName="Correct Answer(s)" name="correct_answers" nameLabel="Shooting_Range_property_correct_answer" type="string"/>
        </property>
            <property displayName="Initial Time For Answer" name="initial_time_for_answer" nameLabel="Shooting_Range_property_initial_time_for_answer" type="string"/>
            <property displayName="Time For Last Answer" name="time_for_last_answer" nameLabel="Shooting_Range_property_time_for_last_answer" type="string"/>
    </model>
<css>.addon-Shooting_Range-wrapper{
    width: 100%;
    height: 100%;
    border: none;
    position: absolute;
}

.addon-Shooting_Range-wrapper-question {
    background-color: lightgray;
    width: 100%;
}

.addon-Shooting_Range-wrapper-level {
    background-color: lightgray;
    width: 100%;
}

.addon-Shooting_Range-wrapper-answers-wrapper {
    top: 0px;
    bottom: 0px;
    background-color: darkgray;
    width: 100%;
    position:absolute;
    height: 100%;
    overflow: hidden;
}

.addon-Shooting_Range-answer-wrapper.clicked {
    background-color: darkslategray;
}

.addon-Shooting_Range-answer-wrapper.isHidden {
    display: none!important;
}

.addon-Shooting_Range-answer-wrapper.correct {
    background-color: green;
}

.addon-Shooting_Range-answer-wrapper.wrong {
    background-color: red;
}

.addon-Shooting_Range-answer-0 {
    position: relative;
    float: left;
    left: 20px;
}

.addon-Shooting_Range-answer-1 {
    position: relative;
    float: left;
    left: 50%;
}

.addon-Shooting_Range-answer-2 {
    position: relative;
    float: right;
    right: 20px;
}

.addon-Shooting_Range-answer-layer {
    top: 0px;
    position: absolute;
    width: 100%;
    height: 100%;
}

.addon-Shooting_Range-answer-wrapper {
    border: 1px solid black;
    background-color: lightslategray;
}

.addon-Shooting_Range-play-button-wrapper {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 99%;
    height: 40px;
    background-color: whitesmoke;
    border-radius: 2px;
    -moz-transform: translateX(-50%) translateY(-50%);
    -webkit-transform: translateX(-50%) translateY(-50%);
    transform: translateX(-50%) translateY(-50%);
}

.addon-Shooting_Range-button-play {
    -moz-transform: translateX(50%) translateY(50%);
    -webkit-transform: translateX(50%) translateY(50%);
    transform: translateX(50%) translateY(50%);
}
</css><view>&lt;div class="addon-Shooting_Range-wrapper"&gt;
    &lt;div class="addon-Shooting_Range-wrapper-question"&gt;&lt;/div&gt;
    &lt;div class="addon-Shooting_Range-wrapper-level"&gt;&lt;/div&gt;
    &lt;div class="addon-Shooting_Range-wrapper-answers-wrapper"&gt;&lt;/div&gt;
    &lt;div class="addon-Shooting_Range-play-button-wrapper"&gt;
        &lt;div class="addon-Shooting_Range-button-play"&gt;
            Play
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="addon-Shooting_Range-wrapper"&gt;
    &lt;div class="addon-Shooting_Range-wrapper-question"&gt;&lt;/div&gt;
    &lt;div class="addon-Shooting_Range-wrapper-level"&gt;&lt;/div&gt;
    &lt;div class="addon-Shooting_Range-wrapper-answers-wrapper"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonShooting_Range_create() {
    var presenter = function (){};

    presenter.state = {
        eventBus: null,
        playerController: null,
        view: null,
        $view: null,
        $questionDiv: null,
        $levelDiv: null,
        $answersWrapper: null,
        $playButton: null,
        levels: [],
        actualLevel: 0,
        destoyed: false,
        errorCount: 0,
        wholeErrorCount: 0,
        score: 0,
        isVisible: false,
        isShowingAnswers: false,
        showingErrors: false,
        isStarted: false,
        resultsList: []
    };

    presenter.configuration = {
        isValid: false,
        addonID: null,
        definitions: [{
            isValid: true,
            gameMode: 0,
            definition: "",
            answers: [""],
            correctAnswers: [0]
        }],
        gameMode: 0,
        initialTimeForAnswer: 0,
        timeForLastAnswer: 0
    };

    presenter.ERROR_CODES = {
        "IT01": "Initial timer for answer must be positive float",
        "LT01": "Time for last answer must be positive float",
        "WM01": "All or non definition in definition list must be provided",
        "EA01": "Answer can't be empty",
        "WA01": "Answer range must be in number-number format",
        "WA02": "First value in answer range must be integer in range &lt;1;3&gt;",
        "WA03": "Second value in answer range must be integer in range &lt;1;3&gt;",
        "WA04": "Correct answer must be integer in range &lt;1;3&gt;"
    };

    presenter.GAME_MODE = {
        UNDEFINED: -1,
        QUESTIONS: 0,
        SPEED_ATTACK: 1
    };

    presenter.originalDisplay = "block";

    presenter.setPlayerController = function (controller) {
        presenter.state.playerController = controller;
        presenter.state.eventBus = presenter.state.playerController.getEventBus();
        presenter.state.eventBus.addEventListener('ShowAnswers', this);
        presenter.state.eventBus.addEventListener('HideAnswers', this);
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.initialize = function  (view, model, isPreview)  {
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.state.$view = $(view);
        presenter.state.view = view;
        presenter.state.$questionDiv = $(view).find(".addon-Shooting_Range-wrapper-question");
        presenter.state.$levelDiv = $(view).find(".addon-Shooting_Range-wrapper-level");
        presenter.state.$answersWrapper = $(view).find(".addon-Shooting_Range-wrapper-answers-wrapper");
        presenter.state.$playButton = $(view).find(".addon-Shooting_Range-play-button-wrapper");

        if (!isPreview) {
           presenter.initializeGame();
           presenter.actualizeAnswersWrapperHeight();
           presenter.connectHandlers();
        }

        var display = presenter.state.$view.css('display');
        if (display != null &amp;&amp; display.length &gt; 0) {
            presenter.originalDisplay = display;
        }

        presenter.setVisibility(presenter.configuration.isVisibleByDefault || isPreview);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.state.view);
        MutationObserverService.setObserver();
    };

    presenter.connectHandlers = function () {
        presenter.state.$playButton.click(function (event) {
            event.preventDefault();
            event.stopPropagation();

            if (presenter.state.isShowingAnswers || presenter.state.showingErrors) {
                return;
            }

            presenter.startGame();
            presenter.state.$playButton.hide();
        });
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorCheckingMode) {
            return;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isAllOK': presenter.isAllOK,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'getResultsList': presenter.getResultsList,
            'restartGame': presenter.restartGame
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getResultsList = function () {
        return presenter.state.resultsList;
    };

    presenter.restartGame = function () {
        var levelsLength = presenter.state.levels.length;

        for (var i = 0; i &lt; levelsLength; i++) {
            presenter.state.levels[i].destroy();
        }

        presenter.state.score = 0;
        presenter.state.errorCount = 0;
        presenter.state.wholeErrorCount = 0;
        presenter.state.$playButton.show();
        presenter.state.actualLevel = 0;
        presenter.actualizeAnswersWrapperHeight();
        presenter.state.isStarted = false;
        presenter.state.$questionDiv.html("&amp;nbsp;");
        presenter.state.$levelDiv.html("&amp;nbsp;");

    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function () {
        presenter.state.isShowingAnswers = true;

        if (!presenter.state.isStarted || presenter.state.$playButton.is(":visible")) {
            return;
        }

        if (presenter.state.showingErrors) {
            presenter.setWorkMode();
        }

        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().showAnswers();
            presenter.getActualLevel().pause();
        }
    };

    presenter.hideAnswers = function () {
        presenter.state.isShowingAnswers = false;

        if (!presenter.state.isStarted || presenter.state.$playButton.is(":visible")) {
            return;
        }

        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().hideAnswers();
            presenter.getActualLevel().resume();
        }
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore();
    };

    presenter.actualizeAnswersWrapperHeight = function () {
        presenter.state.$answersWrapper.css({
            'top': presenter.state.$questionDiv.height() + presenter.state.$levelDiv.height(),
            'height': presenter.state.$view.height() - presenter.state.$questionDiv.height()
        });
    };

    presenter.initializeGame = function () {
        var initialTimerForAnswer = presenter.configuration.initialTimeForAnswer;
        var timeForLastAnswer = presenter.configuration.timeForLastAnswer;
        if (timeForLastAnswer == -1) {
            timeForLastAnswer = initialTimerForAnswer;
        }

        var diffOnLevel = initialTimerForAnswer;
        if (presenter.configuration.definitions.length &gt; 1) {
            diffOnLevel = (timeForLastAnswer - initialTimerForAnswer) / (presenter.configuration.definitions.length - 1);
        }


        for (var i = 0; i &lt; presenter.configuration.definitions.length; i++) {
            var configuration = {
                definition: presenter.configuration.definitions[i],
                timeForAnswer: initialTimerForAnswer + (diffOnLevel * i),
                questionNumber: i,
                numberOfLevel: (i + 1) + "/" + presenter.configuration.definitions.length,
                $levelDiv: presenter.state.$levelDiv,
                $questionDiv: presenter.state.$questionDiv,
                $answersWrapper: presenter.state.$answersWrapper,
                onCorrectAnswerCallback: presenter.onCorrectAnswerCallback.bind(this),
                onWrongAnswerCallback: presenter.onWrongAnswerCallback.bind(this),
                onDroppedCorrectAnswerCallback: presenter.onDroppedCorrectAnswerCallback.bind(this),
                onEndLevelCallback: presenter.onEndLevelCallback.bind(this)
            };
            var level = new Level(configuration);
            presenter.state.levels.push(level);
        }
    };

    presenter.startGame = function () {
        if (presenter.state.isStarted) {
            if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
                presenter.getActualLevel().resume(true);
            }
        } else {
            presenter.getActualLevel().start();
            presenter.state.isStarted = true;
        }
        presenter.actualizeAnswersWrapperHeight();
        presenter.mainLoop();
    };

    presenter.mainLoop = function () {
        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().actualize();
        }

        if (!presenter.state.destoyed) {
            requestAnimationFrame()(presenter.mainLoop);
        }
    };

    presenter.generateEventData = function (questionNumber, answerNumber, isCorrect, clicked) {
        var value = "0";
        if (clicked) {
            value = "1";
        }

        var score = "0";
        if (isCorrect) {
            score = "1";
        }

        return {
            source : presenter.configuration.addonID,
            item : questionNumber + "-" + answerNumber,
            value : value,
            score : score
        };
    };

    presenter.onCorrectAnswerCallback = function (questionNumber, answerNumber) {
        presenter.state.score += 1;
        presenter.sendValueChangedEvent(presenter.generateEventData(questionNumber, answerNumber, true, true));
    };

    presenter.onWrongAnswerCallback = function (questionNumber, answerNumber) {
        presenter.state.errorCount += 1;
        presenter.state.wholeErrorCount += 1;
        presenter.sendValueChangedEvent(presenter.generateEventData(questionNumber, answerNumber, false, true));
    };

    presenter.onDroppedCorrectAnswerCallback = function (questionNumber, answerNumber) {
        presenter.state.errorCount += 1;
        presenter.state.wholeErrorCount += 1;
        presenter.sendValueChangedEvent(presenter.generateEventData(questionNumber, answerNumber, false, false));
    };

    presenter.sendEndOfGameEvent = function () {
        presenter.sendValueChangedEvent({
            source : presenter.configuration.addonID,
            item : 'all',
            value : 'EOG',
            score : '1'
        });
    };

    presenter.sendValueChangedEvent = function (eventData) {
        presenter.state.eventBus.sendEvent('ValueChanged', eventData);
    };

    /**
     * If all elements dropped then call this function
     * @param {boolean} dontPushResults Don't push results to the resultsList. It is used while loading state
     */
    presenter.onEndLevelCallback = function (dontPushResults) {
        presenter.state.actualLevel++;
        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().start();
            if (presenter.state.isShowingAnswers) {
                presenter.getActualLevel().showAnswers();
            }
            presenter.actualizeAnswersWrapperHeight();
        } else {
            if (!dontPushResults) {
                presenter.state.resultsList.push({
                    score: presenter.state.score,
                    errors: presenter.state.wholeErrorCount
                });
                presenter.sendEndOfGameEvent();
            }
        }
    };

    presenter.changeLevel = function (levelIndex) {
        presenter.getActualLevel().destroy();

        presenter.state.actualLevel = levelIndex;
        if (presenter.state.isShowingAnswers) {
            presenter.state.levels[levelIndex].showAnswers();
        }
        presenter.actualizeAnswersWrapperHeight();
    };

    presenter.destroy = function () {
        presenter.state.destoyed = true;

        for (var i = 0; i &lt; presenter.state.levels.length; i++) {
            presenter.state.levels[i].destroy();
        }

        presenter.state.$playButton.off();
    };

    presenter.validateModel = function (model) {
        var validatedInitialTime = presenter.validateInitialTimeForAnswer(model);

        if (!validatedInitialTime.isValid) {
            return validatedInitialTime;
        }

        var validatedTimeForLastAnswer = presenter.validateTimeForLastAnswer(model);
        if (!validatedInitialTime.isValid) {
            return validatedTimeForLastAnswer;
        }

        var validatedDefinitions = presenter.validateDefinitions(model);
        if (!validatedDefinitions.isValid) {
            return validatedDefinitions;
        }

        return {
            isValid: true,
            initialTimeForAnswer: validatedInitialTime.initialTimeForAnswer,
            timeForLastAnswer: validatedTimeForLastAnswer.timeForLastAnswer,
            definitions: validatedDefinitions.definitions,
            gameMode: validatedDefinitions.gameMode,
            addonID: model['ID'],
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model['Is Visible'])
        };
    };

    presenter.validateDefinitions = function (model) {
        var definitionsUnvalidated = model['definitions'];
        var definitionsValidated = [];
        var gameMode = presenter.GAME_MODE.UNDEFINED;


        for (var i = 0; i &lt; definitionsUnvalidated.length; i++) {
            var validatedDefinition = presenter.validateDefinition(definitionsUnvalidated[i]);
            if (!validatedDefinition.isValid) {
                return validatedDefinition;
            }

            var validatedGameMode = presenter.validateGameMode(gameMode, validatedDefinition.gameMode);
            if (!validatedGameMode.isValid) {
                return validatedGameMode;
            }
            gameMode = validatedGameMode.gameMode;

            definitionsValidated.push(validatedDefinition);
        }

        return {
            isValid: true,
            definitions: definitionsValidated,
            gameMode: gameMode
        };
    };

    presenter.validateDefinition = function (definitionUnvalidated) {
        var gameMode = presenter.GAME_MODE.UNDEFINED;

        if (htmlIsEmpty(definitionUnvalidated['definition'])) {
            definitionUnvalidated['definition'] = "";
            gameMode = presenter.GAME_MODE.SPEED_ATTACK;
        } else {
            gameMode = presenter.GAME_MODE.QUESTIONS;
        }

        var answer1 = definitionUnvalidated['answer1'];
        var answer2 = definitionUnvalidated['answer2'];
        var answer3 = definitionUnvalidated['answer3'];

        if (window.ModelValidationUtils.isStringEmpty(answer1)) {
            return generateValidationError("EA01");
        }

        if (window.ModelValidationUtils.isStringEmpty(answer2)) {
            return generateValidationError("EA01");
        }

        if (window.ModelValidationUtils.isStringEmpty(answer3)) {
            return generateValidationError("EA01");
        }

        var validatedCorrectAnswers = presenter.validateCorrectAnswers(definitionUnvalidated['correct_answers']);
        if (!validatedCorrectAnswers.isValid) {
            return validatedCorrectAnswers;
        }

        return {
            isValid: true,
            gameMode: gameMode,
            definition: definitionUnvalidated['definition'],
            answers: [answer1, answer2, answer3],
            correctAnswers: validatedCorrectAnswers.correctAnswers
        }
    };

    function htmlIsEmpty(htmlValue) {
        var $container = $("&lt;div&gt;&lt;/div&gt;");
        $container.html(htmlValue);
        var text = $container.text();
        return window.ModelValidationUtils.isStringEmpty(text);
    }

    presenter.validateCorrectAnswers = function (correctAnswerValue) {
        var correctAnswers = [];

        if (!window.ModelValidationUtils.isStringEmpty(correctAnswerValue)) {
            if (correctAnswerValue.indexOf("-") &gt; -1) {
                var range = getCorrectAnswersFromRange(correctAnswerValue);
                if (!range.isValid) {
                    return range;
                }

                correctAnswers = range.correctAnswers;
            } else {
                var answers = getCorrectAnswersSeparatedByComma(correctAnswerValue);
                if (!answers.isValid) {
                    return answers;
                }

                correctAnswers = answers.correctAnswers;
            }
        }

        return {
            isValid: true,
            correctAnswers: correctAnswers
        };
    };

    function getCorrectAnswersSeparatedByComma(answers) {
        var correctAnswers = [];
        var splitAnswers = answers.split(",");

        for (var i = 0; i &lt; splitAnswers.length; i++) {
            var validatedCorrectAnswer = window.ModelValidationUtils.validateIntegerInRange(splitAnswers[i], 3, 1);
            if (!validatedCorrectAnswer.isValid) {
                return generateValidationError("WA04");
            }

            correctAnswers.push(validatedCorrectAnswer.value - 1);
        }

        correctAnswers = window.ModelValidationUtils.removeDuplicatesFromArray(correctAnswers);
        correctAnswers = correctAnswers.sort();

        return {
            isValid: true,
            correctAnswers: correctAnswers
        };
    }

    function getCorrectAnswersFromRange(range) {
        var correctAnswers = [];
        var splitRange = range.split("-");

        if (splitRange.length !== 2) {
            return generateValidationError("WA01");
        }

        var start = window.ModelValidationUtils.validateIntegerInRange(splitRange[0], 3, 1);
        if (!start.isValid) {
            return generateValidationError("WA02");
        }

        var end = window.ModelValidationUtils.validateIntegerInRange(splitRange[1], 3, 1);
        if (!end.isValid) {
            return generateValidationError("WA03");
        }

        for (var i = Math.min(start.value, end.value) - 1; i &lt;= Math.max(start.value, end.value) - 1; i++) {
            correctAnswers.push(i);
        }

        return {
            isValid: true,
            correctAnswers: correctAnswers
        }

    }

    presenter.validateGameMode = function (lastGameMode, newGameMode) {
        if (lastGameMode == presenter.GAME_MODE.UNDEFINED || lastGameMode == newGameMode) {
            return {
                isValid: true,
                gameMode: newGameMode
            };
        }

        if (lastGameMode !== newGameMode) {
            return generateValidationError("WM01");
        }
    };

    presenter.validateTimeForLastAnswer = function (model) {
        var timeForLastAnswerUnvalidated = model['time_for_last_answer'];

        if (window.ModelValidationUtils.isStringEmpty(timeForLastAnswerUnvalidated)) {
            return {
                isValid: true,
                timeForLastAnswer: -1
            };
        }

        var validatedFloat = window.ModelValidationUtils.validateFloatInRange(timeForLastAnswerUnvalidated, Number.MAX_VALUE, 0.01);
        if (!validatedFloat.isValid) {
            return generateValidationError("LT01")
        }

        return {
            isValid: true,
            timeForLastAnswer: validatedFloat.parsedValue
        };

    };

    presenter.validateInitialTimeForAnswer = function (model) {
        var initialTimeForAnswerUnvalidated = model['initial_time_for_answer'];
        var validatedFloat = window.ModelValidationUtils.validateFloatInRange(initialTimeForAnswerUnvalidated, Number.MAX_VALUE, 0.01);
        if (!validatedFloat.isValid) {
            return generateValidationError("IT01")
        }

        return {
            isValid: true,
            initialTimeForAnswer: validatedFloat.parsedValue
        }
    };

    presenter.setVisibility = function (isVisible) {
        presenter.state.$view.css('visibility', isVisible ? 'visible' : 'hidden');
        presenter.state.$view.css('display', isVisible ? presenter.originalDisplay : 'none');

        presenter.state.isVisible = isVisible;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.reset = function () {
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);

        presenter.setWorkMode();
        presenter.hideAnswers();
        presenter.state.resultsList = [];
        presenter.restartGame();
    };

    presenter.setShowErrorsMode = function() {
        presenter.state.showingErrors = true;

        if (presenter.state.$playButton.is(":visible")) {
            return;
        }
        if (presenter.state.isShowingAnswers) {
            presenter.hideAnswers();
        }

        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().pause();
        }
    };

    presenter.setWorkMode = function () {
        presenter.state.showingErrors = false;

        if (!presenter.state.isStarted || presenter.state.$playButton.is(":visible")) {
            return;
        }

        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            presenter.getActualLevel().resume();
        }
    };

    presenter.setState = function (stateString) {
        var state = JSON.parse(stateString);

        if (state.isStarted) {
            presenter.changeLevel(state['actualLevel']);
            presenter.getActualLevel().start(state['actualLevelTimeElapsed'], state['clickedElements']);

            presenter.actualizeAnswersWrapperHeight();
            if (!state.isFinished) {
                presenter.getActualLevel().actualize();
                presenter.getActualLevel().pause(true);
            }
            presenter.state.isStarted = true;
        }
        presenter.state.errorCount = state.errorCount;
        presenter.state.wholeErrorCount = state.wholeErrorCount;
        presenter.state.score = state.score;
        presenter.state.resultsList = state.resultsList;

        if (state.isFinished) {
            presenter.getActualLevel().destroy();
            presenter.onEndLevelCallback(true);
        }

        presenter.setVisibility(state.isVisible);
    };

    presenter.getActualLevel = function () {
        return presenter.state.levels[presenter.state.actualLevel];
    };

    presenter.getState = function () {
        var actualLevelTimeElapsed = 1;
        var isFinished = true;
        var actualLevel = presenter.state.levels.length - 1;
        var clickedElements = [];
        var isStarted = presenter.state.isStarted;

        if (presenter.state.actualLevel &lt; presenter.state.levels.length) {
            actualLevelTimeElapsed = presenter.getActualLevel().getElapsedTime();
            isFinished = false;
            actualLevel = presenter.state.actualLevel;
            clickedElements = presenter.getActualLevel().getClicked();
        }

        var state = {
            actualLevel: actualLevel,
            actualLevelTimeElapsed: actualLevelTimeElapsed,
            isFinished: isFinished,
            isVisible: presenter.state.isVisible,
            score: presenter.state.score,
            errorCount: presenter.state.errorCount,
            wholeErrorCount: presenter.state.wholeErrorCount,
            clickedElements: clickedElements,
            isStarted: isStarted,
            resultsList: presenter.state.resultsList
        };

        return JSON.stringify(state);
    };

    presenter.getScore = function () {
        return presenter.state.score;
    };

    presenter.getMaxScore = function () {
        var definitionsLength = presenter.configuration.definitions.length;
        var maxScore = 0;
        for (var i = 0; i &lt; definitionsLength; i++) {
            maxScore += presenter.configuration.definitions[i].correctAnswers.length;
        }

        return maxScore;
    };

    presenter.getErrorCount = function () {
        var lastErrorCount = presenter.state.errorCount;

        return lastErrorCount;
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorCheckingMode) {
            return;
        }

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };
        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.createEventData = function () {
        return {
            source : presenter.configuration.addonID,
            item : "all",
            value : '',
            score : ''
        };

    };

    function generateValidationError(errorCode) {
        return {
            isValid: false,
            errorCode: errorCode
        };
    }

    presenter.__internalElements = {
        Level: Level
    };

    function Level (configuration) {
        this.definition = configuration.definition;
        this.questionNumber = configuration.questionNumber;
        this.$answersWrapper = configuration.$answersWrapper;
        this.$questionDiv = configuration.$questionDiv;
        this.$levelDiv = configuration.$levelDiv;
        this.numberOfLevel = configuration.numberOfLevel;
        this.timeForAnswer = configuration.timeForAnswer;
        this.callbacks = {
            onCorrectAnswerCallback: configuration.onCorrectAnswerCallback,
            onWrongAnswerCallback: configuration.onWrongAnswerCallback,
            onDroppedCorrectAnswerCallback: configuration.onDroppedCorrectAnswerCallback,
            onEndLevelCallback: configuration.onEndLevelCallback
        };

        this.initialElapsedTime = 0;
        this.startTime = 0;
        this.answers = [];
        this.levelWasEnded = false;
        this.destroyed = true;
        this.droppedElements = 0;
        this.clickedElements = 0;
        this.pauseTime = 0;
        this.isPaused = false;
    }

    Level.prototype = {
        start: function (elapsedTime, clickedElements) {
            this.clickedElements = 0;
            this.startTime = new Date().getTime() / 1000;
            this.generateAnswers();
            this.$questionDiv.html(this.definition["definition"]);
            this.$levelDiv.html(this.numberOfLevel);
            this.destroyed = false;
            this.initialElapsedTime = 0;
            this.pauseTime = 0;
            this.isPaused = false;
            this.levelWasEnded = false;
            this.droppedElements = 0;

            if (this.definition.gameMode == presenter.GAME_MODE.SPEED_ATTACK) {
                this.$questionDiv.hide();
            } else {
                this.$questionDiv.show();
            }

            if (elapsedTime) {
                this.setElapsedTime(elapsedTime);
            }

            if (clickedElements) {
                this.setClicked(clickedElements);
            }
        },

        destroy: function () {
            if (this.destroyed) {
                return;
            }
            this.destroyed = true;

            for (var i = 0; i &lt; 3; i++) {
                this.answers[i].element.off();
                this.answers[i].element.remove();
            }
        },

        actualize: function () {
            if (this.destroyed || this.isPaused) {
                return;
            }

            this.speedForSecond = this.$answersWrapper.height() / this.timeForAnswer;

            var timeElapsed =(new Date().getTime() / 1000) - this.startTime + this.initialElapsedTime;
            var top = timeElapsed * this.speedForSecond;

            for (var i = 0; i &lt; 3; i++) {
                this.answers[i].element.css({'top': top});
                if (top &gt; this.$answersWrapper.height()) {
                    this.onDrop(this.questionNumber, i);
                }
            }
        },

        generateAnswers: function () {
            this.answers = [];
            for (var i = 0; i &lt; 3; i++) {
                this.answers.push(this.generateAnswer(i));
            }
        },

        generateAnswer: function (index) {
            var wrapper = $("&lt;div&gt;&lt;/div&gt;");
            wrapper.addClass("addon-Shooting_Range-answer-wrapper addon-Shooting_Range-answer-" + index);

            var text = $("&lt;div&gt;&lt;/div&gt;");
            text.addClass("addon-Shooting_Range-answer-text");

            var layer = $("&lt;div&gt;&lt;/div&gt;");
            layer.addClass("addon-Shooting_Range-answer-layer");

            wrapper.append(text);
            wrapper.append(layer);

            var isCorrect = $.inArray(index, this.definition.correctAnswers) &gt; -1;
            var functionOnClick = this.onClick.bind(this, this.questionNumber, isCorrect, index);

            layer.mousedown(functionOnClick);
            layer.on('touchstart', functionOnClick);
            text.html(this.definition.answers[index]);

            this.$answersWrapper.append(wrapper);

            return {
                element: wrapper,
                text: text,
                layer: layer,
                isClicked: false,
                isCorrect: isCorrect
            };
        },

        onClick: function (questionNumber, isCorrect, answerNumber, event) {
            event.stopPropagation();
            event.preventDefault();

            if(this.isPaused) {
                return;
            }

            if (this.answers[answerNumber].isClicked) {
                return;
            }

            if (isCorrect) {
                this.callbacks.onCorrectAnswerCallback(questionNumber, answerNumber);
                this.setCorrectAnswer(answerNumber);
            } else {
                this.callbacks.onWrongAnswerCallback(questionNumber, answerNumber);
                this.setWrongAnswer(answerNumber);
            }
            this.setClickedAnswer(answerNumber);
        },

        onDrop: function (questionNumber, answerNumber) {
            this.droppedElements++;

            if (!this.answers[answerNumber].isClicked) {
                var isCorrect = this.answers[answerNumber].isCorrect;
                if (isCorrect) {
                    this.callbacks.onDroppedCorrectAnswerCallback(questionNumber, answerNumber);
                }
            }

            if (this.droppedElements == 3) {
                this.endLevel();
            }
        },

        endLevel: function () {
            if (this.levelWasEnded) {
                return;
            }

            this.levelWasEnded = true;
            this.destroy();
            this.callbacks.onEndLevelCallback();
        },

        setElapsedTime: function (elapsed) {
            this.initialElapsedTime = elapsed;
        },

        getElapsedTime: function () {
            if (this.startTime == 0) {
                return 0;
            }

            var pausedTime = 0;

            if (this.isPaused) {
                pausedTime = this.pauseTime - new Date().getTime() / 1000
            }

            return (new Date().getTime() / 1000) - this.startTime + this.initialElapsedTime + pausedTime;
        },

        setWrongAnswer(answerNumber) {
            this.answers[answerNumber].element.addClass("wrong");
        },

        setCorrectAnswer(answerNumber) {
            this.answers[answerNumber].element.addClass("correct");
        },

        setClickedAnswer(answerNumber) {
            this.answers[answerNumber].isClicked = true;
            this.answers[answerNumber].element.addClass("clicked");
            this.clickedElements++;
        },


        setClicked: function (clickedArray) {
            for (var i = 0; i &lt; clickedArray.length; i++) {
                this.setClickedAnswer(clickedArray[i]);
                if(this.answers[clickedArray[i]].isCorrect){
                    this.setCorrectAnswer(clickedArray[i]);
                }else{
                    this.setWrongAnswer(clickedArray[i]);
                }
            }
        },

        getClicked: function () {
            var clicked = [];

            if (this.destroyed) {
                return clicked;
            }

            for (var  i = 0; i &lt; 3; i++) {
                if (this.answers[i].isClicked) {
                    clicked.push(i);
                }
            }

            return clicked;
        },

        showAnswers: function () {
            if (this.destroyed) {
                return;
            }
            for (var  i = 0; i &lt; 3; i++) {
                if (this.answers[i].isCorrect) {
                    this.answers[i].element.addClass('correct');
                } else {
                    this.answers[i].element.addClass('wrong');
                }
            }
        },

        hideAnswers: function () {
            if (this.destroyed) {
                return;
            }
            for (var  i = 0; i &lt; 3; i++) {
                this.answers[i].element.removeClass('correct');
                this.answers[i].element.removeClass('wrong');
            }
        },

        pause: function (hideElements) {
            if (this.isPaused) {
                return;
            }
            this.isPaused = true;
            this.pauseTime = new Date().getTime() / 1000;

            if (hideElements) {
                for (var i = 0; i &lt; 3; i++) {
                    this.answers[i].element.addClass("isHidden");
                }
            }
        },

        resume: function (removeIsHidden) {
            if (!this.isPaused) {
                return;
            }
            this.isPaused = false;
            this.initialElapsedTime += this.pauseTime - new Date().getTime() / 1000;
            if (removeIsHidden) {
                for (var i = 0; i &lt; 3; i++) {
                    this.answers[i].element.removeClass("isHidden");
                }
            }
        }
    };

    //https://stackoverflow.com/questions/5605588/how-to-use-requestanimationframe
    function requestAnimationFrame () {
        return window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function(/* function */ callback){
                window.setTimeout(callback, 1000 / 60);
            };
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Show_Answers" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isLocalized="true" name="Text" nameLabel="Show_Answers_property_text" type="string"/>
        <property isLocalized="true" name="Text selected" nameLabel="Show_Answers_property_text_selected" type="string"/>
        <property name="Increment check counter" nameLabel="Show_Answers_property_increment_check_counter" type="boolean"/>
        <property name="Increment mistake counter" nameLabel="Show_Answers_property_increment_mistake_counter" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="choice_speech_texts" type="staticlist">

            <property name="Selected" nameLabel="choice_item_selected" type="staticrow">
                <property name="Selected" nameLabel="choice_item_selected" type="string"/>
            </property>

            <property name="Block edit" nameLabel="check_answers_button_edit_block" type="staticrow">
                <property name="Block edit" nameLabel="check_answers_button_edit_block" type="string"/>
            </property>

            <property name="No block edit" nameLabel="check_answers_button_no_edit_block" type="staticrow">
                <property name="No block edit" nameLabel="check_answers_button_no_edit_block" type="string"/>
            </property>

        </property>
    </model>
<css>.show-answers-wrapper,
.show-answers-wrapper .show-answers-container,
.show-answers-wrapper .show-answers-container .show-answers-button {
    width: 100%;
    height: 100%;
}

.show-answers-wrapper .show-answers-container .show-answers-button {
    background: url('resources/show-answers-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}
</css><view>&lt;div class="show-answers-wrapper"&gt;
    &lt;div class="show-answers-container"&gt;
        &lt;div class="show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="show-answers-wrapper"&gt;
    &lt;div class="show-answers-container"&gt;
        &lt;div class="show-answers-button"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonShow_Answers_create(){
    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    var isWCAGOn = false;

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function getTextVoiceObject (text, lang) {
        return {
            text: text,
            lang: lang
        };
    }

    presenter.EVENTS = {
        SHOW_ANSWERS: 'ShowAnswers',
        HIDE_ANSWERS: 'HideAnswers'
    };

    presenter.keyboardController = function(keycode, isShiftDown, event) {
        if (keycode == window.KeyboardControllerKeys.SPACE ||
            keycode == window.KeyboardControllerKeys.ARROW_UP ||
            keycode == window.KeyboardControllerKeys.ARROW_DOWN ||
            keycode == window.KeyboardControllerKeys.ESC)
        {
            event.preventDefault();
        }

        if (keycode === window.KeyboardControllerKeys.ENTER) {
            presenter.$button.click();
            if(isWCAGOn) {
                if (presenter.configuration.isSelected) {
                    speak([getTextVoiceObject(presenter.speechTexts.editBlock)]);
                } else {
                    speak([getTextVoiceObject(presenter.speechTexts.noEditBlock)]);
                }
            }
        }
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.sendEvent = function(eventName) {
        var source = presenter.configuration.addonID;
        if (eventName === presenter.EVENTS.SHOW_ANSWERS) {
            presenter.playerController.getCommands().showAnswers(source);
        } else if (eventName === presenter.EVENTS.HIDE_ANSWERS) {
            presenter.playerController.getCommands().hideAnswers(source);
        } else {
            var eventData = {
                'source': source
            };
            // fallback - but it should use commands for proper work with different modes
            presenter.eventBus.sendEvent(eventName, eventData);
        }

    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.validateModel = function(model) {
        presenter.setSpeechTexts(model['speechTexts']);
        return {
            'text' : model.Text,
            'textSelected' : model['Text selected'],
            'isVisible' : ModelValidationUtils.validateBoolean(model["Is Visible"]),
            'addonID' : model.ID,
            'isSelected': false,
            'enableCheckCounter': ModelValidationUtils.validateBoolean(model["Increment check counter"]),
            'enableMistakeCounter': ModelValidationUtils.validateBoolean(model["Increment mistake counter"]),
            'isTabindexEnabled': ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"])
        };
    };

    presenter.setSpeechTexts = function(speechTexts){
        presenter.speechTexts = {
            selected: 'Selected',
            editBlock: 'Page edition is blocked',
            noEditBlock: 'Page edition is not blocked'
        };

        if(!speechTexts){
            return;
        }

        presenter.speechTexts = {
            selected: getSpeechTextProperty(speechTexts['Selected']['Selected'], presenter.speechTexts.selected),
            editBlock: getSpeechTextProperty(speechTexts['Block edit']['Block edit'], presenter.speechTexts.editBlock),
            noEditBlock: getSpeechTextProperty(speechTexts['No block edit']['No block edit'], presenter.speechTexts.noEditBlock)
        };
    };

    presenter.upgradeModel = function (model) {
        if (model["Increment mistake counter"] === undefined) {
            model = presenter.upgradeIncrementMistakeCounter(model);
        }
        return model;
    };

     presenter.upgradeIncrementMistakeCounter = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel["Increment mistake counter"] === undefined) {
            upgradedModel["Increment mistake counter"] = "false";
        }

        return upgradedModel;
    };

    presenter.handleClick = function () {
        var text, eventName;

        presenter.configuration.isSelected = !presenter.configuration.isSelected;

        if (presenter.configuration.isSelected) {
            text = presenter.configuration.textSelected;
            eventName = presenter.EVENTS.SHOW_ANSWERS;
            presenter.$wrapper.addClass('selected');

            if (presenter.configuration.enableCheckCounter) {
                presenter.playerController.getCommands().incrementCheckCounter();
            }

            if (presenter.configuration.enableMistakeCounter) {
                presenter.playerController.getCommands().increaseMistakeCounter();
            }
        } else {
            text = presenter.configuration.text;
            eventName = presenter.EVENTS.HIDE_ANSWERS;
            presenter.$wrapper.removeClass('selected');
        }

        presenter.$button.text(text);
        presenter.sendEvent(eventName);
        presenter.onClick();
    };

    presenter.onClick = function () {
     };

    presenter.connectClickAction = function () {
        presenter.$button.on('click', function (eventData) {
            eventData.stopPropagation();
            presenter.handleClick();
        });
    };

    presenter.connectKeyDownAction = function () {
        presenter.$view.on('keydown', function (eventData) {
            if(eventData.which === 13) {
                eventData.stopPropagation();
                presenter.handleClick();
            }
        });
    };

    presenter.presenterLogic = function(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.configuration = presenter.validateModel(upgradedModel);
        presenter.$view = $(view);

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        presenter.$button = presenter.$view.find('.show-answers-button');
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper = presenter.$view.find('.show-answers-wrapper');

        if (presenter.configuration.isTabindexEnabled) {
            presenter.$wrapper.attr('tabindex', '0');
        }

        if (!isPreview) {
            presenter.connectClickAction();
            presenter.connectKeyDownAction();
            presenter.eventBus.addEventListener('ShowAnswers', presenter);
            presenter.eventBus.addEventListener('HideAnswers', presenter);
            presenter.eventBus.addEventListener('LimitedHideAnswers', presenter);
        }
    };

    presenter.run = function(view, model) {
        presenter.view = view;
        presenter.presenterLogic(view, model, false);

        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        presenter.$button.off();
        presenter.$view.off();

        presenter.$button = null;
        presenter.$wrapper = null;
        presenter.$view = null;
        presenter.view = null;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.configuration.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName == "LimitedHideAnswers") {
            presenter.reset();
        }
        if (eventName == "HideAnswers") {
            presenter.reset();
        }
        if (eventName == "ShowAnswers") {
            presenter.$button.text(presenter.configuration.textSelected);
            presenter.$wrapper.addClass('selected');
            presenter.configuration.isSelected = true;
        }
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            "show": presenter.show,
            "hide": presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getState = function() {
        return JSON.stringify({
            'isVisible' : presenter.configuration.isVisible
        });
    };

    presenter.setState = function(state) {
        presenter.setVisibility(JSON.parse(state).isVisible);
    };

    presenter.reset = function () {
        presenter.$button.text(presenter.configuration.text);
        presenter.$wrapper.removeClass('selected');
        presenter.configuration.isSelected = false;
    };

    presenter.setShowErrorsMode = function () {
        presenter.reset();
    };

    presenter.setWorkMode = function () {
        presenter.reset();
    };

    presenter.getTitlePostfix = function () {
        if(presenter.configuration.isSelected) {
            return presenter.speechTexts.selected;
        } else {
            return ''
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts) {
            tts.speak(data);
        }
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Single_State_Button" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="Title" nameLabel="Single_State_Button_property_title" type="string"/>
		<property name="Image" nameLabel="Single_State_Button_property_image" type="image"/>
		<property isDefault="true" name="onClick" nameLabel="Single_State_Button_property_on_click" type="event"/>
		<property name="Disable" nameLabel="Single_State_Button_property_disable" type="boolean"/>
		<property name="Enable in error mode" nameLabel="Single_State_Button_property_enable_in_error_mode" type="boolean"/>
		<property name="Enable in show answers mode" nameLabel="Single_State_Button_property_enable_in_show_answers_mode" type="boolean"/>
		<property displayName="Render SVG as HTML" name="renderSVGAsHTML" nameLabel="Single_State_Button_property_render_SVG_as_HTML" type="boolean"/>
	</model>
<css>.singlestate-button-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.singlestate-button-wrapper div.svg-element {
    height: auto!important;
}

.singlestate-button-element {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    background: url('resources/ssb-button.png') no-repeat center;
    cursor: pointer;
}

.singlestate-button-title,
.singlestate-button-image {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    cursor: pointer;
    position: absolute;
}
</css><view>&lt;div class="singlestate-button-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;div class="singlestate-button-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonSingle_State_Button_create() {
    var presenter = function() {};

    presenter.DISPLAY_CONTENT_TYPE = {
        NONE: 0,
        TITLE: 1,
        IMAGE: 2,
        BOTH: 3
    };

    presenter.ERROR_MESSAGES = {
        INVALID_FILE: 'Invalid SVG file',
    };

    presenter.state = {
        isErrorMode: false
    };

    presenter.executeUserEventCode = function() {
        if (presenter.playerController == null) return;
        if (presenter.configuration.onClickEvent.isEmpty) return;

        presenter.playerController.getCommands().executeEventCode(presenter.configuration.onClickEvent.value);
    };

    presenter.clickHandler = function (event) {
        if (event !== undefined) {
            event.stopPropagation();
        }

        var allowClickInErrorMode = presenter.state.isErrorMode &amp;&amp; presenter.configuration.enableInErrorMode;
        if (presenter.configuration.isDisabled) return;
        if (presenter.configuration.isErrorMode &amp;&amp; !allowClickInErrorMode) return;
        if (presenter.configuration.isShowAnswersMode &amp;&amp; !presenter.configuration.enableInShowAnswersMode) return;

        presenter.executeUserEventCode();
        presenter.triggerButtonClickedEvent();
    };

    function handleMouseActions() {
        var $element = presenter.$view.find('div[class*=singlestate-button-element]:first');
        $element.click(presenter.clickHandler);
    }

    function setElementsDimensions(model, wrapper, element) {
        var viewDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var viewDistances = DOMOperationsUtils.calculateOuterDistances(viewDimensions);
        presenter.$view.css({
            width:(model.Width - viewDistances.horizontal) + 'px',
            height:(model.Height - viewDistances.vertical) + 'px'
        });

        DOMOperationsUtils.setReducedSize(presenter.$view, wrapper);
        DOMOperationsUtils.setReducedSize(wrapper, element);
    }

    function createImageElement(element) {
        $(element).removeClass('svg-element');
        if (presenter.configuration.renderSVGAsHTML) {
            $(element).addClass('svg-element');
            createSVGElement(element);
        } else {
            var $imageElement = $(document.createElement('img'));
            $imageElement.addClass('singlestate-button-image');
            $imageElement.attr('src', presenter.configuration.image);
            $(element).append($imageElement);
        }
    }

    function createTitleElement(element) {
        var $titleElement = $(document.createElement('span'));
        $titleElement.addClass('singlestate-button-title');
        $titleElement.html(presenter.configuration.title);
        $(element).append($titleElement);
    }

    function createElements(wrapper) {
        var $element = $(document.createElement('div'));
        $element.addClass('singlestate-button-element');
        if (presenter.configuration.isTabindexEnabled) {$element.attr('tabindex', '0');}

        switch (presenter.configuration.displayContent) {
            case presenter.DISPLAY_CONTENT_TYPE.TITLE:
                createTitleElement($element);

                break;
            case presenter.DISPLAY_CONTENT_TYPE.IMAGE:
                createImageElement($element);

                break;
            case presenter.DISPLAY_CONTENT_TYPE.BOTH:
                createImageElement($element);
                createTitleElement($element);

                break;
        }

        wrapper.append($element);

        return $element;
    }

    function createSVGElement(element) {
        const sanitizeUrl = window.xssUtils.sanitize(presenter.configuration.image);

        $.ajax({
            url: sanitizeUrl,
            success: function (data) {
                onSVGLoadCompleted(data, element);
            },
            error: function () {
                onSVGLoadError();
            },
            dataType: 'xml'
        });
    }

    function onSVGLoadError () {
        const errorContainer = '&lt;p&gt;' + presenter.ERROR_MESSAGES.INVALID_FILE + '&lt;/p&gt;';
        presenter.$view.html(errorContainer);
    }

    function onSVGLoadCompleted(data, element) {
        const svgElement = $(data).find('svg');
        if (svgElement.length === 0) {
            onSVGLoadError();
            return;
        }

        const svgWidth = getElementWidth(svgElement);
        const svgHeight = getElementHeight(svgElement);

        svgElement.attr('viewBox', '0 0 ' + svgWidth + ' ' + svgHeight);
        svgElement.attr('width', '100%');
        svgElement.attr('height', '100%');

        const svgData = data.childNodes;
        $(element).html(svgData);
        createTitleElement(element);
    }

    function getElementWidth(HTMLElement) {
        const width = HTMLElement.attr('width');
        const widthFromRectTag = HTMLElement.find('rect').attr('width');

        if (width) {
            return width.replace('px', '');
        } else if (widthFromRectTag) {
            return widthFromRectTag.replace('px', '');
        } else {
            return presenter.configuration.width;
        }
    }

    function getElementHeight(HTMLElement) {
        const height = HTMLElement.attr('width');
        const heightFromRectTag = HTMLElement.find('rect').attr('width');

        if (height) {
            return height.replace('px', '');
        } else if (heightFromRectTag) {
            return heightFromRectTag.replace('px', '');
        } else {
            return presenter.configuration.height;
        }
    }

    function presenterLogic(view, model, isPreview) {
        presenter.addonID = model.ID;
        presenter.$view = $(view);

        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        var $wrapper = $(presenter.$view.find('.singlestate-button-wrapper:first')[0]);
        var $element = createElements($wrapper);

        setElementsDimensions(upgradedModel, $wrapper, $element);
        presenter.toggleDisable(presenter.configuration.isDisabledByDefault);
        presenter.setVisibility(presenter.configuration.isVisibleByDefault || isPreview);

        if (!isPreview) {
            handleMouseActions();
        }
    }

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;

        var eventBus = presenter.playerController.getEventBus();

        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeDisable(model);
        upgradedModel = presenter.upgradeEnableInErrorMode(upgradedModel);
        upgradedModel =  presenter.upgradeEnableInShowAnswersMode(upgradedModel);
        return presenter.addRenderSVGAsHTML(upgradedModel);
    };

    presenter.upgradeDisable = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel.Disable) {
            upgradedModel.Disable = "False";
        }

        return upgradedModel;
    };

    presenter.upgradeEnableInErrorMode = function AddonSingleStateButton_upgradeEnableInErrorMode(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel['Enable in error mode']) {
            upgradedModel['Enable in error mode'] = "False";
        }

        return upgradedModel;
    }

    presenter.upgradeEnableInShowAnswersMode = function AddonSingleStateButton_upgradeEnableInErrorMode(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel['Enable in show answers mode']) {
            upgradedModel['Enable in show answers mode'] = "False";
        }

        return upgradedModel;
    }

    presenter.addRenderSVGAsHTML = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!model.hasOwnProperty('renderSVGAsHTML')) {
            upgradedModel['renderSVGAsHTML'] = 'False';
        }

        return upgradedModel;
    };

    presenter.validateString = function (imageSrc) {
        var isEmpty = ModelValidationUtils.isStringEmpty(imageSrc);

        return {
            isEmpty: isEmpty,
            value: isEmpty ? "" : imageSrc
        };
    };

    presenter.determineDisplayContent = function(title, image) {
        var displayContent = presenter.DISPLAY_CONTENT_TYPE.NONE;

        if (!title.isEmpty &amp;&amp; image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.TITLE;
        } else if (title.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.IMAGE;
        } else if (!title.isEmpty &amp;&amp; !image.isEmpty) {
            displayContent = presenter.DISPLAY_CONTENT_TYPE.BOTH;
        }

        return displayContent;
    };

    presenter.validateModel = function (model) {
        var title = presenter.validateString(model.Title);
        var image = presenter.validateString(model.Image);
        var onClickEvent = presenter.validateString(model.onClick);
        var isDisabled = ModelValidationUtils.validateBoolean(model.Disable);
        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']);
        var enableInErrorMode = ModelValidationUtils.validateBoolean(model['Enable in error mode']);
        var enableInShowAnswersMode = ModelValidationUtils.validateBoolean(model['Enable in show answers mode']);
        var renderSVGAsHTML = ModelValidationUtils.validateBoolean(model.renderSVGAsHTML);

        return {
            displayContent: presenter.determineDisplayContent(title, image),
            title: title.value,
            image: image.value,
            onClickEvent: onClickEvent,
            isDisabled: isDisabled,
            isDisabledByDefault: isDisabled,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isErrorMode: false,
            isTabindexEnabled: isTabindexEnabled,
            enableInErrorMode: enableInErrorMode,
            enableInShowAnswersMode: enableInShowAnswersMode,
            renderSVGAsHTML: renderSVGAsHTML
        };
    };

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enable,
            'disable': presenter.disable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.createEventData = function() {
        return {
            source : presenter.addonID,
            item : '',
            value : '1',
            score : ''
        };
    };

    presenter.triggerButtonClickedEvent = function() {
        if (presenter.playerController == null) return;

        presenter.playerController.getEventBus().sendEvent('ValueChanged', this.createEventData());
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        this.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        this.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.isEnabledInGSAMode = function () {
        return presenter.configuration.enableInShowAnswersMode;
    }

    presenter.reset = function() {
        presenter.setWorkMode();

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        if (presenter.configuration.isVisible) {
            this.show();
        } else {
            this.hide();
        }
        presenter.toggleDisable(this.configuration.isDisabledByDefault);
    };

    presenter.enable = function() {
        this.toggleDisable(false);
    };

    presenter.disable = function() {
        this.toggleDisable(true);
    };

    presenter.toggleDisable = function(disable) {
        var element = presenter.$view.find('div[class*=singlestate-button-element]:first');
        if(disable) {
            element.addClass("disable");
        } else {
            element.removeClass("disable");
        }
        presenter.configuration.isDisabled = disable;
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            isDisabled: presenter.configuration.isDisabled
        });
    };

    presenter.setState = function(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        var state = JSON.parse(stateString);
        presenter.configuration.isDisabled = state.isDisabled;
        presenter.configuration.isVisible = state.isVisible;

        if (presenter.configuration.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.toggleDisable(presenter.configuration.isDisabled);
    };

    presenter.setShowErrorsMode = function () {
        presenter.configuration.isErrorMode = true;
        presenter.state.isErrorMode = true;
    };

    presenter.setWorkMode = function () {
        presenter.configuration.isErrorMode = false;
        presenter.state.isErrorMode = false;
        presenter.configuration.isShowAnswersMode = false;
    };

    presenter.onEventReceived = function (eventName) {
        if (eventName === "ShowAnswers") {
            presenter.configuration.isShowAnswersMode = true;
        }

        if (eventName === "HideAnswers") {
            presenter.configuration.isShowAnswersMode = false;
            presenter.setWorkMode();
        }
    };

    presenter.keyboardController = function(keyCode, isShiftDown, event) {
        event.preventDefault();
        if (keyCode === window.KeyboardControllerKeys.ENTER) {
            presenter.clickHandler();
        }
    };
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Slider" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="ImageElement" nameLabel="Slider_property_image_element" type="image"/>
		<property name="Orientation" nameLabel="Slider_property_orientation" type="{Landscape, Portrait}"/>
		<property name="Stepwise" nameLabel="Slider_property_stepwise" type="boolean"/>
		<property name="StepsCount" nameLabel="Slider_property_steps_count" type="string"/>
		<property name="InitialStep" nameLabel="Slider_property_initial_step" type="string"/>
		<property name="onStepChange" nameLabel="Slider_property_on_step_change" type="event"/>
		<property name="Block in error checking mode" nameLabel="Slider_property_block_in_error_checking_mode" type="boolean"/>
		<property name="Continuous events" nameLabel="Slider_property_continuous_events" type="boolean"/>
        <property name="Continuous events steps" nameLabel="Slider_property_continuous_events_steps" type="{Stick, Smooth}"/>
		<property name="Alternative texts" nameLabel="Slider_property_alternative_texts" type="list">
			<property name="Alternative text" nameLabel="Slider_property_alternative_text" type="text"/>
			<property name="Step number" nameLabel="Slider_property_step_number" type="string"/>
		</property>
		<property name="langAttribute" nameLabel="Slider_property_lang_attribute" type="string"/>
		<property name="speechTexts" nameLabel="Slider_property_speech_texts" type="staticlist">
			<property name="Step" nameLabel="Slider_property_step" type="staticrow">
				<property name="Step" nameLabel="Slider_property_step" type="string"/>
			</property>
		</property>
		<property name="StepwiseModeBarAlwaysVisible" nameLabel="Slider_property_stepwise_mode_bar_always_visible" type="boolean"/>
	</model>
<css>.slider-wrapper {
    border: 0 solid black;
}
.slider-element-image-mouse-hover,
.slider-element-image-mouse-click,
.slider-element-image {
    position: relative;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    border: 0 solid black;
    -ms-touch-action: none;
}
</css><view>&lt;div class="slider-wrapper"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="slider-wrapper"&gt;

&lt;/div&gt;
</preview><presenter>function AddonSlider_create () {
    var presenter = function () {};

    presenter.$view = null;
    presenter.savedState = null;
    presenter.counter = 0;
    presenter.isTouched = false;
    presenter.isWCAGOn = false;

    var playerController, onStepChangeEvent;

    presenter.ORIENTATION = {
        LANDSCAPE : 0, // Horizontal
        PORTRAIT : 1 // Vertical
    };

    presenter.ERROR_CODES = {
        'ES_01' : "Element source was not given!",
        'SC_01' : "Steps count incorrect!",
        'SC_02' : "Steps count cannot be less than 2!",
        'IS_01' : "Initial step incorrect! It must be a positive number between 1 and steps count!",
        'AT_01' : "Step number must be a positive number between 1 and steps count",
        "AT_02" : "Step number has been provided but the alternative text is missing",
        "AT_03" : "Duplicate step number!"
    };

    presenter.addonID = '';

    presenter.mouseData = {
        isMouseDown : false,
        oldPosition : {
            x : 0,
            y : 0
        },
        isMouseDragged : false
    };

    var imageElementData = {
        width : 0,
        height : 0,
        maxLeft : 0,
        maxTop : 0
    };

    var CLASSES_NAMES = {
        WRAPPER : {
            STANDARD_CLASS : 'slider-wrapper',
            SELECTOR : '.slider-wrapper:first'
        },
        ELEMENT_IMAGE : {
            STANDARD_CLASS : 'slider-element-image',
            MOUSE_HOVER : 'slider-element-image-mouse-hover',
            MOUSE_CLICK : 'slider-element-image-mouse-click',
            SELECTOR : 'div[class*="slider-element-image"]:first'
        }
    };

    /**
     * Trigger a callback when the selected images are loaded:
     * @param {String} selector
     * @param {Function} callback
     */
    var onImgLoaded = function($element, callback){
        if ($element[0].complete) {
            callback($element[0]);
        }
        else {
            $element.on('load', function(){
                callback($element[0]);
            });
        }
    };

    function loadImageElement(isPreview) {
        var addonContainer = presenter.$addonContainer;
        var imageElement = document.createElement('img');
        $(imageElement).attr('src', presenter.configuration.imageSrc + "?" + new Date().getTime()); // fix for IE 10 cached images http://css-tricks.com/snippets/jquery/fixing-load-in-ie-for-cached-images/
        addonContainer.html(imageElement);

        onImgLoaded($(imageElement), function(image) {
            var width = image.width;
            var height = image.height;
            imageElementData.width = width;
            imageElementData.height = height;
            imageElementData.maxLeft = $(addonContainer).width() - width;
            imageElementData.maxTop = $(addonContainer).height() - height;

            var imageContainer = document.createElement('div');

            $(imageContainer).addClass(CLASSES_NAMES.ELEMENT_IMAGE.STANDARD_CLASS);
            $(imageContainer).css({
                backgroundImage: "url('" + presenter.configuration.imageSrc + "')",
                backgroundSize : '100% 100%',
                width: imageElementData.width + 'px',
                height: imageElementData.height + 'px'
            });
            addonContainer.html(imageContainer);

            var containerLength = presenter.configuration.orientation === presenter.ORIENTATION.LANDSCAPE ? $(addonContainer).width() : $(addonContainer).height();

            var elementLength = presenter.configuration.orientation === presenter.ORIENTATION.LANDSCAPE ? $(imageContainer).width() : $(imageContainer).height();
            if(!presenter.configuration.stepwise) {
                presenter.configuration.stepsCount = containerLength - elementLength + 1;
            }
            var stepZoneLength = (containerLength - elementLength) / (presenter.configuration.stepsCount - 1);



            presenter.configuration.snapPoints.push(elementLength / 2);
            for (var i = 0; i &lt; presenter.configuration.stepsCount - 2; i++) {
                var snapPoint = elementLength / 2 + (i + 1) * stepZoneLength;
                presenter.configuration.snapPoints.push(parseInt(snapPoint, 10));
            }
            presenter.configuration.snapPoints.push(containerLength - elementLength / 2);

            presenter.moveToStep(imageContainer, presenter.configuration.initialStep, presenter.configuration);

            if (!isPreview) {
                presenter.configuration.stepwiseModeBarAlwaysVisible &amp;&amp; drawBurret();
                handleMouseDrag(addonContainer);
            } else {
                drawBurret();
            }

            presenter.setVisibility(presenter.configuration.isVisibleByDefault);

            presenter.imageElement = imageContainer;

            presenter.imageLoadedDeferred.resolve();
        });
    }

    presenter.moveToStep = function(element, step, configuration) {
        var elementContainer = presenter.$addonContainer;
        var containerLength = configuration.orientation === presenter.ORIENTATION.LANDSCAPE ? $(elementContainer).width() : $(elementContainer).height();
        var elementLength = configuration.orientation === presenter.ORIENTATION.LANDSCAPE ? $(element).width() : $(element).height();
        var zoneLength = containerLength - elementLength;
        var singleStepZoneLength = (containerLength - elementLength) / (configuration.stepsCount - 1);
        var distance = singleStepZoneLength * (step - 1);

        if (distance &lt; 0 || step === 1) {
            distance = 0;
        }

        if (distance &gt; zoneLength || step === configuration.stepsCount) {
            distance = zoneLength;
        }

        $(element).css({
            top : configuration.orientation === presenter.ORIENTATION.PORTRAIT ? distance : 0,
            left : configuration.orientation === presenter.ORIENTATION.LANDSCAPE ? distance : 0
        });

        var tempElement = document.createElement('p');
        elementContainer.append(tempElement);
        $(elementContainer).find('p:first').remove();

    };


    function mouseDownCallback (eventData) {
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.shouldBlockInErrorMode) return;

        presenter.mouseData.isMouseDown = true;
        presenter.mouseData.oldPosition.x = eventData.pageX;
        presenter.mouseData.oldPosition.y = eventData.pageY;
        if (eventData.stopPropagation) eventData.stopPropagation();
        if (eventData.preventDefault) eventData.preventDefault();
    }

    function touchStartCallback (event) {
        presenter.isTouched = true;

        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];
        
        var touch = event.touches[0] || touchPoints[0];
        mouseDownCallback(touch);
    }

    presenter.mouseUpEventDispatcher = function (event) {
        if (presenter.mouseData.isMouseDown) {
            presenter.mouseUpHandler(event);
        } else {
            return;
        }
    };

    presenter.mouseUpHandler = function (event) {
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.shouldBlockInErrorMode) {
            return;
        }

        $(presenter.imageElement).removeClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_CLICK);

        var addonContainer = presenter.$view.find(CLASSES_NAMES.WRAPPER.SELECTOR);
        var imageElement = $(addonContainer.find(CLASSES_NAMES.ELEMENT_IMAGE.SELECTOR))[0];

        presenter.mouseData.isMouseDown = false;
        if (presenter.configuration.newStep !== presenter.configuration.currentStep) {
            presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = presenter.configuration.newStep;
            presenter.triggerOnStepChangeUserEvent();

            presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }

        if (presenter.configuration.stepwise) {
            presenter.moveToStep(imageElement, presenter.configuration.currentStep, presenter.configuration);
        }
    };

    function touchEndCallback (event) {
        presenter.isTouched = false;
        event.preventDefault();
        event.stopPropagation();

        presenter.mouseUpEventDispatcher();
    }

    function mouseClickCallback (eventData) {

        eventData.stopPropagation();

        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.shouldBlockInErrorMode) return;

        var addonContainer = presenter.$view.find(CLASSES_NAMES.WRAPPER.SELECTOR);
        var imageElement = $(addonContainer.find(CLASSES_NAMES.ELEMENT_IMAGE.SELECTOR))[0];

        $(presenter.imageElement).addClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_CLICK);
        $(presenter.imageElement).removeClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_HOVER);

        if (presenter.mouseData.isMouseDragged) {
            presenter.mouseData.isMouseDragged = false;
            return;
        }

        var mousePositions = getMousePositions(eventData);
        presenter.configuration.newStep = presenter.whichStepZone(mousePositions, presenter.configuration);

        if (presenter.configuration.newStep !== presenter.configuration.currentStep) {
            presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = presenter.configuration.newStep;
            presenter.triggerOnStepChangeUserEvent();

            presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }

        presenter.moveToStep(imageElement, presenter.configuration.currentStep, presenter.configuration);
    }

    function mouseMoveCallback (eventData) {
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.shouldBlockInErrorMode) return;
        var addonContainer = presenter.$addonContainer;
        var imageElement = presenter.imageElement;

        if (presenter.mouseData.isMouseDown) {
            presenter.mouseData.isMouseDragged = true;
            var mousePositions = getMousePositions(eventData);
            var relativeDistance;

            if(presenter.isTouched) {
                var scale = playerController.getScaleInformation();
                if (scale.scaleX !== 1.0 || scale.scaleY !== 1.0) {
                    mousePositions.x = mousePositions.x / scale.scaleX;
                    mousePositions.y = mousePositions.y / scale.scaleY;
                }
            }

            if(presenter.continuousEvents &amp;&amp; presenter.continuousEventsSteps == "Smooth"){
                presenter.configuration.newStep = presenter.whichStepZoneSmooth(mousePositions, presenter.configuration);
            }else{
                presenter.configuration.newStep = presenter.whichStepZone(mousePositions, presenter.configuration);
            }

            if ( presenter.configuration.orientation == presenter.ORIENTATION.LANDSCAPE ) {
                relativeDistance = presenter.calculateRelativeDistanceX(imageElement, addonContainer, eventData, presenter.mouseData, imageElementData);
                presenter.mouseData.oldPosition.x = eventData.pageX;

                var minimumXPosition = ($(imageElement).width() / 2);
                var maximumXPosition = imageElementData.maxLeft + ($(imageElement).width() / 2);

                mousePositions.x = mousePositions.x &gt; minimumXPosition ? mousePositions.x : minimumXPosition;
                mousePositions.x = mousePositions.x &lt; maximumXPosition? mousePositions.x : maximumXPosition;

                if(!presenter.continuousEvents || (presenter.continuousEvents &amp;&amp; presenter.continuousEventsSteps == "Smooth")){
                    $(imageElement).css({
                        left: (mousePositions.x + relativeDistance.horizontal - ($(imageElement).width() / 2)) + 'px'
                    });
                }

                if (presenter.configuration.newStep !== presenter.configuration.currentStep &amp;&amp; presenter.continuousEvents) {
                    presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

                    presenter.configuration.currentStep = presenter.configuration.newStep;
                    presenter.triggerOnStepChangeUserEvent();

                    presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);

                    if(presenter.continuousEventsSteps == "Stick" || presenter.continuousEventsSteps == undefined || presenter.continuousEventsSteps == ""){
                        presenter.moveToStep(imageElement, presenter.configuration.currentStep, presenter.configuration);
                    }
                }

            } else {
                relativeDistance = presenter.calculateRelativeDistanceY(imageElement, addonContainer, eventData, presenter.mouseData, imageElementData);
                var minimumYPosition = ($(imageElement).height() / 2);
                var maximumYPosition = imageElementData.maxTop + ($(imageElement).height() / 2);
                mousePositions.y = mousePositions.y &gt; minimumYPosition ? mousePositions.y : minimumYPosition;
                mousePositions.y = mousePositions.y &lt; maximumYPosition ? mousePositions.y : maximumYPosition;

                presenter.mouseData.oldPosition.y = eventData.pageY;

                if(!presenter.continuousEvents || (presenter.continuousEvents &amp;&amp; presenter.continuousEventsSteps == "Smooth")){
                    $(imageElement).css({
                        top: (mousePositions.y + relativeDistance.vertical - ($(imageElement).height() / 2)) + 'px'
                    });
                }

                if (presenter.configuration.newStep !== presenter.configuration.currentStep &amp;&amp; presenter.continuousEvents) {
                    presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

                    presenter.configuration.currentStep = presenter.configuration.newStep;
                    presenter.triggerOnStepChangeUserEvent();

                    presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);

                    if(presenter.continuousEventsSteps == "Stick" || presenter.continuousEventsSteps == undefined || presenter.continuousEventsSteps == ""){
                        presenter.moveToStep(imageElement, presenter.configuration.currentStep, presenter.configuration);
                    }
                }
            }
        }
        if (eventData.preventDefault &amp;&amp; eventData.target &amp;&amp; presenter.$view.find(eventData.target).length) {
            eventData.preventDefault();
        }
    }

    function touchMoveCallback (event) {
        event.stopPropagation();
        event.preventDefault();

        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        var touch = event.touches[0] || touchPoints[0];
        mouseMoveCallback(touch);
    }

    function handleMouseDrag(addonContainer) {
        const $icplayer = $('#_icplayer');
        presenter.isWindowsMobile = false;

        if (window.navigator.msPointerEnabled &amp;&amp; MobileUtils.isMobileUserAgent(window.navigator.userAgent)) {
            presenter.isWindowsMobile = true;
        }

        var imageElement = $(addonContainer.find(CLASSES_NAMES.ELEMENT_IMAGE.SELECTOR))[0];

        $(imageElement).hover(
            function() {
                $(this).toggleClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_HOVER, !presenter.mouseData.isMouseDown);
            },
            function() {
                $(this).toggleClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_HOVER, presenter.mouseData.isMouseDown);
            }
        );

        if (presenter.isWindowsMobile) {
            imageElement.addEventListener('MSPointerDown', touchStartCallback, false);
            imageElement.addEventListener('MSPointerMove', touchMoveCallback, false);
        } else {
            imageElement.ontouchstart = touchStartCallback;
            imageElement.ontouchmove = touchMoveCallback;
        }

        $(imageElement).mousedown(mouseDownCallback);
        $icplayer.mousemove(mouseMoveCallback);
        $icplayer.mouseup(presenter.mouseUpEventDispatcher);
        $(document).mouseup(presenter.mouseUpEventDispatcher);
        imageElement.ontouchend = touchEndCallback;

        $(addonContainer).click(mouseClickCallback);


        $(imageElement).hover(function() {
            $(presenter.imageElement).addClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_HOVER);
        }, function() {
            $(presenter.imageElement).removeClass(CLASSES_NAMES.ELEMENT_IMAGE.MOUSE_HOVER);
        });
    }

    function getMousePositions(eventData) {
        var popupTop = 0,
            popupLeft = 0;

        var $popup = presenter.$view.parent('.ic_popup_page').offset();
        if ($popup !== null) {
            var popupTop = $popup.top;
            var popupLeft = $popup.left;
        }

        setAddonPosition();

        if (eventData.offsetX != null &amp;&amp; eventData.offsetY != null) {
            position = {
                x:(eventData.offsetX + $(eventData.target).offset().left - presenter.configuration.offset.left) - popupLeft,
                y:(eventData.offsetY + $(eventData.target).offset().top - presenter.configuration.offset.top) - popupTop
            };
        } else {
            var scroll = getScroll();
            position = {
                x: (scroll.left + eventData.pageX - presenter.configuration.offset.left) - popupLeft,
                y: (scroll.top + eventData.pageY - presenter.configuration.offset.top) - popupTop
            }
        }

        return position;
    }

    function getScroll() {
        var top = $('body').scrollTop();
        var left = $('body').scrollLeft();
        return {top: top, left: left};
    }

    function setAddonPosition() {
        presenter.configuration.offset = {};
        presenter.configuration.offset.left = presenter.$addonContainer.offset().left;
        presenter.configuration.offset.top = presenter.$addonContainer.offset().top;
    }

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeTTS(model);
        upgradedModel = presenter.upgradeStepwiseModeBarAlwaysVisible(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeTTS = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel['speechTexts'] === undefined) {
            upgradedModel['speechTexts'] = {
                Step: {Step: "Step"}
            };
        }

        if (upgradedModel['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] = "";
        }

        if (upgradedModel['Alternative texts'] === undefined) {
            upgradedModel['Alternative texts'] = [
                {
                    "Alternative text": "",
                    "Step number": ""
                }
            ];
        }

        return upgradedModel;
    };

    presenter.upgradeStepwiseModeBarAlwaysVisible = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty("StepwiseModeBarAlwaysVisible")) {
            upgradedModel["StepwiseModeBarAlwaysVisible"] = "False";
        }

        return upgradedModel;
    };

    function presenterLogic(view, model, preview) {
        var upgradedModel = presenter.upgradeModel(model);

        presenter.imageLoadedDeferred = new jQuery.Deferred();
        presenter.imageLoaded = presenter.imageLoadedDeferred.promise();

        presenter.addonID = upgradedModel.ID;
        presenter.$view = $(view);
        presenter.view = view;
        onStepChangeEvent = upgradedModel.onStepChange;
        presenter.continuousEvents = ModelValidationUtils.validateBoolean(upgradedModel["Continuous events"]);
        presenter.continuousEventsSteps = upgradedModel["Continuous events steps"];

        presenter.$addonContainer = presenter.$view.find(CLASSES_NAMES.WRAPPER.SELECTOR);

        DOMOperationsUtils.setReducedSize(presenter.$view, presenter.$addonContainer);

        presenter.configuration = presenter.convertModel(upgradedModel);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        setAddonPosition();

        presenter.configuration.currentStep = presenter.configuration.initialStep;
        presenter.configuration.newStep = presenter.configuration.initialStep;
        presenter.configuration.snapPoints = [];

        loadImageElement(preview);

        presenter.$view.disableSelection();
        MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    }

    function drawBurret() {
        if (!presenter.configuration.stepwise) return;

        var xPosition = $(presenter.$view).width() / 2;
        var yPosition = $(presenter.$view).height() / 2;
        var verticalLineLength = $(presenter.$view).height() / 4;
        var horizontalLineLength = $(presenter.$view).width() / 4;

        var canvas = document.createElement('canvas');
        $(canvas).css({
            position : 'absolute',
            top : 0,
            left : 0
        });
        $(canvas).attr('width', $(presenter.$view).width());
        $(canvas).attr('height', $(presenter.$view).height());
        $(canvas).addClass('slider-stepwise-bar');
        $(presenter.$view).prepend(canvas);

        if (presenter.configuration.orientation === presenter.ORIENTATION.LANDSCAPE) {
            $(canvas).drawLine({
                strokeStyle:"#000",
                strokeWidth:2,
                strokeCap:"round",
                x1:presenter.configuration.snapPoints[0], y1:yPosition,
                x2:presenter.configuration.snapPoints[presenter.configuration.stepsCount - 1], y2:yPosition
            });
        } else {
            $(canvas).drawLine({
                strokeStyle:"#000",
                strokeWidth:2,
                strokeCap:"round",
                x1:xPosition, y1:presenter.configuration.snapPoints[0],
                x2:xPosition, y2:presenter.configuration.snapPoints[presenter.configuration.stepsCount - 1]
            });
        }

        for (var i = 0; i &lt; presenter.configuration.stepsCount; i++) {
            if (presenter.configuration.orientation === presenter.ORIENTATION.LANDSCAPE) {
                $(canvas).drawLine({
                    strokeStyle:"#000",
                    strokeWidth:2,
                    strokeCap:"round",
                    x1:presenter.configuration.snapPoints[i], y1:yPosition - verticalLineLength / 2,
                    x2:presenter.configuration.snapPoints[i], y2:yPosition + verticalLineLength / 2
                });
            } else {
                $(canvas).drawLine({
                    strokeStyle:"#000",
                    strokeWidth:2,
                    strokeCap:"round",
                    x1:xPosition - horizontalLineLength / 2, y1:presenter.configuration.snapPoints[i],
                    x2:xPosition + horizontalLineLength / 2, y2:presenter.configuration.snapPoints[i]
                });
            }
        }
    }

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.setPlayerController = function(controller) {
        playerController = controller;
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.triggerOnStepChangeUserEvent = function () {
        if (!onStepChangeEvent) return;

        playerController.getCommands().executeEventCode(onStepChangeEvent);
    };

    presenter.getContainerAndImageElements = function () {
        var addonContainer = presenter.$view.find(CLASSES_NAMES.WRAPPER.SELECTOR);
        var imageElement = addonContainer.find(CLASSES_NAMES.ELEMENT_IMAGE.SELECTOR)[0];

        return {
            addonContainer: addonContainer,
            imageElement: imageElement
        };
    };

    presenter.moveToInitialStepCommand = function (params) {
        var elements = this.getContainerAndImageElements();
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 0);

        if(presenter.configuration.currentStep !== presenter.configuration.initialStep) {
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = presenter.configuration.initialStep;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.moveToInitialStep = function (triggerEvent) {
        presenter.moveToInitialStepCommand([triggerEvent + '']);
    };

    presenter.moveToCommand = function (params) {
        var elements = this.getContainerAndImageElements();
        var step = parseInt(params[0], 10);
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 1);

        if (!isNaN(step) &amp;&amp; step &gt;= 1 &amp;&amp; step &lt;= presenter.configuration.stepsCount) {
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = step;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.moveTo = function (step, triggerEvent) {
        presenter.moveToCommand([step, triggerEvent + '']);
    };

    presenter.moveToLastCommand = function(params) {
        var elements = this.getContainerAndImageElements();
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 0);

        if (presenter.configuration.currentStep !== presenter.configuration.stepsCount) {
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = presenter.configuration.stepsCount;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.moveToLast = function(triggerEvent) {
        presenter.moveToLastCommand([triggerEvent + '']);
    };

    presenter.moveToFirstCommand = function (params) {
        var elements = this.getContainerAndImageElements();
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 0);

        if (presenter.configuration.currentStep !== 1) {
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep = 1;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.moveToFirst = function (triggerEvent) {
        presenter.moveToFirstCommand([triggerEvent + '']);
    };

    presenter.nextStepCommand = function (params) {
        var elements = this.getContainerAndImageElements();
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 0);

        if (presenter.configuration.currentStep + 1 &lt;= presenter.configuration.stepsCount) {
            if (triggerEvent)
                presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep++;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent)
                presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.nextStep = function (triggerEvent) {
        presenter.nextStepCommand([triggerEvent + '']);
    };

    presenter.previousStepCommand = function (params) {
        var elements = this.getContainerAndImageElements();
        var triggerEvent = presenter.parseAdditionalTriggerEventParam(params, 0);

        if (presenter.configuration.currentStep - 1 &gt;= 1) {
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, false);

            presenter.configuration.currentStep--;
            presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

            presenter.triggerOnStepChangeUserEvent();
            if (triggerEvent) presenter.triggerStepChangeEvent(presenter.configuration.currentStep, true);
        }
    };

    presenter.previousStep = function (triggerEvent) {
        presenter.previousStepCommand([triggerEvent + '']);
    };

    presenter.getCurrentStep = function () {
        return presenter.configuration.currentStep.toString();
    };

    presenter.parseAdditionalTriggerEventParam = function (params, numberOfDefaultParams) {
        if (!params[numberOfDefaultParams]) return true;

        return params[numberOfDefaultParams].toLowerCase() != 'false';
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.configuration.isErrorMode &amp;&amp; presenter.configuration.shouldBlockInErrorMode) return;

        var commands = {
            'moveTo': presenter.moveToCommand,
            "moveToLast": presenter.moveToLastCommand,
            'moveToFirst': presenter.moveToFirst,
            'moveToInitialStep' : presenter.moveToInitialStepCommand,
            'nextStep': presenter.nextStep,
            'previousStep': presenter.previousStep,
            'getCurrentStep': presenter.getCurrentStep,
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.convertStepsCount = function (stepsCount) {
        var convertedStepsCount = ModelValidationUtils.validatePositiveInteger(stepsCount);

        if (!convertedStepsCount.isValid) {
            return { isError: true, errorCode: 'SC_01' };
        }

        if (convertedStepsCount.value &lt; 2) {
            return { isError: true, errorCode: 'SC_02' };
        }

        return { isError: false, stepsCount: convertedStepsCount.value };
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();
        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.convertSpeechTexts = function(speechTextsModel) {
        var speechTexts = {
            step: getSpeechTextProperty(speechTextsModel["Step"]["Step"], "Step")
        };
        return speechTexts;
    };

    presenter.validateAlternativeTexts = function(altTextsModel, stepsCount) {
        var altTexts = [];
        for (var i = 0; i &lt; stepsCount; i++) {
            altTexts.push('');
        }

        for (var i = 0; i &lt; altTextsModel.length; i++) {
            var altText = altTextsModel[i];

            if (altText["Step number"].length == 0) continue;

            if (isNaN(altText["Step number"])) {
                return { isError : true, errorCode : 'AT_01' };
            }

            if (
                    (altText["Step number"].length &gt; 0 &amp;&amp; altText["Alternative text"].length == 0) ||
                    (altText["Step number"].length == 0 &amp;&amp; altText["Alternative text"].length &gt; 0)
            ) {
                return { isError : true, errorCode : 'AT_02' };
            }

            var stepNumber = Number(altText["Step number"]);
            if (stepNumber &lt; 1 || stepNumber &gt; stepsCount) {
                return { isError : true, errorCode : 'AT_01' };
            }
            if (altTexts[stepNumber - 1] != '') {
                return { isError : true, errorCode : 'AT_03' };
            }

            altTexts[stepNumber - 1] = altText['Alternative text'];
        }
        return { isError : false, value : altTexts };
    };

    presenter.convertModel = function(model) {
        var orientation = model.Orientation === 'Portrait' ? presenter.ORIENTATION.PORTRAIT : presenter.ORIENTATION.LANDSCAPE;
        var stepwise = ModelValidationUtils.validateBoolean(model.Stepwise);
        var stepsCount = 0;
        var initialStep = 1;

        if (ModelValidationUtils.isStringEmpty(model.ImageElement)) {
            return { isError : true, errorCode : 'ES_01' };
        }

        if (stepwise) {
            var convertedStepsCount = presenter.convertStepsCount(model.StepsCount);
            if (convertedStepsCount.isError) {
                return { isError: true, errorCode: convertedStepsCount.errorCode };
            }

            stepsCount = convertedStepsCount.stepsCount;

            if (ModelValidationUtils.isStringEmpty(model.InitialStep)) {
                initialStep = 1;
            } else {
                var convertedInitialStep = ModelValidationUtils.validateIntegerInRange(model.InitialStep, stepsCount, 1);
                if (!convertedInitialStep.isValid) {
                    return { isError: true, errorCode: 'IS_01' };
                }

                initialStep = convertedInitialStep.value;
            }
        }

        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        var speechTexts = presenter.convertSpeechTexts(model["speechTexts"]);

        var validatedAltTexts = presenter.validateAlternativeTexts(model['Alternative texts'], stepsCount);
        if (validatedAltTexts.isError) {
            return {isError: true, errorCode: validatedAltTexts.errorCode};
        }

        return {
            imageSrc : model.ImageElement,
            orientation : orientation,
            stepwise : stepwise,
            stepsCount : stepsCount,
            initialStep : initialStep,
            isVisibleByDefault: isVisible,
            isVisible: isVisible,
            isError : false,
            isErrorMode: false,
            shouldBlockInErrorMode: ModelValidationUtils.validateBoolean(model["Block in error checking mode"]),
            speechTexts: speechTexts,
            lang: model["langAttribute"],
            altTexts: validatedAltTexts.value,
            stepwiseModeBarAlwaysVisible: ModelValidationUtils.validateBoolean(model["StepwiseModeBarAlwaysVisible"])
        };
    };

    presenter.closestSmooth = 0;
    presenter.whichStepZoneSmooth = function (mousePositions, globalData) {
        var imageCenter = globalData.orientation === presenter.ORIENTATION.LANDSCAPE ? parseInt($(presenter.imageElement).css('left'), 10) +
                ($(presenter.imageElement).width()/2) : parseInt($(presenter.imageElement).css('top'), 10) + ($(presenter.imageElement).height()/2),
            margin = parseInt((globalData.snapPoints[1] - globalData.snapPoints[0]) / 5, 10);

        for (var j = 0; j &lt; globalData.snapPoints.length; j++) {
            var pointBefore = parseInt(globalData.snapPoints[j] - margin, 10);
            var pointAfter = parseInt(globalData.snapPoints[j] + margin, 10);
            if(imageCenter &gt; pointBefore &amp;&amp; imageCenter &lt; pointAfter){
                if(presenter.closestSmooth != j){
                    presenter.closestSmooth = j;
                }
            }
        }

        return presenter.closestSmooth + 1;
    };

    presenter.whichStepZone = function(mousePositions, globalData) {
        var snapPointDistance = [];

        var mousePosition = globalData.orientation === presenter.ORIENTATION.LANDSCAPE ? mousePositions.x : mousePositions.y;

        for (var i = 0; i &lt; globalData.snapPoints.length; i++) {
            snapPointDistance.push({
                distance : Math.abs(mousePosition - globalData.snapPoints[i]),
                snapPoint : i
            });
        }

        var closest = 0;
        for (var j = 1; j &lt; globalData.snapPoints.length; j++) {
            if (snapPointDistance[closest].distance &gt; snapPointDistance[j].distance) {
                closest = j;
            }
        }

        return closest + 1;
    };

    presenter.calculateRelativeDistanceX = function(imageElement, container, eventData, pastEventData, imageElementData) {
        var left = parseInt($(imageElement).css('left'), 10);
        var horizontal = Math.round(eventData.pageX - pastEventData.oldPosition.x);

        var isIE = /*@cc_on!@*/false || !!document.documentMode;

        if(isIE){
            if(horizontal &gt; 8 &amp;&amp; window.screen.deviceXDPI&gt;96){
                horizontal = horizontal/1.5;
            }
        }

        if (left + horizontal &lt; 0 || left + horizontal &gt; imageElementData.maxLeft) {
            horizontal = 0;
        }

        return { left: left, horizontal: horizontal };
    };

    presenter.calculateRelativeDistanceY = function(imageElement, container, eventData, pastEventData, imageElementData) {
        var top = parseInt($(imageElement).css('top'), 10);
        var vertical = eventData.pageY - pastEventData.oldPosition.y;

        if (top + vertical &lt; 0 || top + vertical &gt; imageElementData.maxTop) {
            vertical = 0;
        }

        return { top: top, vertical: vertical };
    };

    presenter.createEventData = function (step, moveIn) {
        return {
            source : presenter.addonID,
            item : "" + step,
            value : moveIn ? "1" : "0",
            score : ''
        };
    };

    presenter.triggerStepChangeEvent = function(step, moveIn) {
        var eventData = this.createEventData(step, moveIn);
        if (playerController != null) {
            playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.addDisabledClass = function () {
        presenter.getContainerAndImageElements().addonContainer.addClass('disabled');
    };

    presenter.removeDisabledClass = function () {
        presenter.getContainerAndImageElements().addonContainer.removeClass('disabled');
    };

    presenter.reset = function () {
        presenter.configuration.isErrorMode = false;
        presenter.removeDisabledClass();
        var elements = this.getContainerAndImageElements();
        presenter.configuration.currentStep = presenter.configuration.initialStep;
        presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);

        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.getState = function() {
        var state = {
            'currentStep' : presenter.configuration.currentStep,
            'isVisible' : presenter.configuration.isVisible
        };
        return JSON.stringify(state);
    };

    presenter.setState = function(stateString) {
        var state = JSON.parse(stateString);

        presenter.configuration.currentStep = state['currentStep'];
        presenter.configuration.isVisible = state['isVisible'];

        $.when(presenter.imageLoaded).then(presenter.loadImagesCallback);
    };

    presenter.loadImagesCallback = function() {
        var elements = presenter.getContainerAndImageElements();

        presenter.moveToStep(elements.imageElement, presenter.configuration.currentStep, presenter.configuration);
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {

        var keys = {
            ENTER: 13,
            ARROW_LEFT: 37,
            ARROW_UP: 38,
            ARROW_RIGHT: 39,
            ARROW_DOWN: 40
        };

        function nextStep () {
            presenter.nextStep(event);
            presenter.readStep(presenter.configuration.currentStep);
        }

        function prevStep () {
            presenter.previousStep(event);
            presenter.readStep(presenter.configuration.currentStep);
        }

        function readCurrentStep () {
            presenter.readStep(presenter.configuration.currentStep);
        }

        var mapping = {};
        mapping[keys.ARROW_LEFT] = prevStep;
        mapping[keys.ARROW_UP] = prevStep;
        mapping[keys.ARROW_DOWN] = nextStep;
        mapping[keys.ARROW_RIGHT] = nextStep;
        mapping[keys.ENTER] = readCurrentStep;

        try {
            mapping[keycode]();
        } catch (er) {
        }

    };

    presenter.isEnterable = function() {
        return false;
    };

    presenter.readStep = function(index) {
        var voices = [];
        var altText = presenter.configuration.altTexts[index - 1];
        if (altText.length == 0) {
            voices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.step + ' ' + index));
        } else {
            voices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.speechTexts.step));
            voices.push(window.TTSUtils.getTextVoiceObject(altText, presenter.configuration.lang));
        }
        presenter.speak(voices);
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }
        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        presenter.isWCAGOn = isOn;
    };

    presenter.speak = function(data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setShowErrorsMode = function() {
        presenter.configuration.isErrorMode = true;

        if (presenter.configuration.shouldBlockInErrorMode) {
            presenter.addDisabledClass();
        }
    };

    presenter.setWorkMode = function() {
        presenter.configuration.isErrorMode = false;
        presenter.removeDisabledClass();
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        const $icplayer = $('#_icplayer');
        $icplayer.off("mousemove", mouseMoveCallback);
        $icplayer.off("mouseup", presenter.mouseUpEventDispatcher);
        $(document).off('mouseup', presenter.mouseUpEventDispatcher);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Slideshow" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Audio" nameLabel="Slideshow_property_audio" type="list">
			<property name="MP3" nameLabel="Slideshow_property_mp3" type="audio"/>
			<property name="OGG" nameLabel="Slideshow_property_ogg" type="audio"/>
		</property>
		<property name="Slides" nameLabel="Slideshow_property_slides" type="list">
			<property name="Image" nameLabel="Slideshow_property_image" type="image"/>
			<property name="Start" nameLabel="Slideshow_property_start" type="string"/>
			<property name="Audiodescription" nameLabel="Slideshow_property_audiodescription" type="text"/>
		</property>
		<property name="Texts" nameLabel="Slideshow_property_texts" type="list">
			<property isLocalized="true" name="Text" nameLabel="Slideshow_property_text" type="string"/>
			<property name="Top" nameLabel="Slideshow_property_top" type="string"/>
			<property name="Left" nameLabel="Slideshow_property_left" type="string"/>
			<property name="Start" nameLabel="Slideshow_property_start" type="string"/>
			<property name="End" nameLabel="Slideshow_property_end" type="string"/>
		</property>
		<property name="No audio" nameLabel="Slideshow_property_no_audio" type="boolean"/>
		<property name="Presentation duration" nameLabel="Slideshow_property_presentation_duration" type="string"/>
		<property name="Slide animation" nameLabel="Slideshow_property_slide_animation" type="boolean"/>
		<property name="Text animation" nameLabel="Slideshow_property_text_animation" type="boolean"/>
		<property name="Hide progressbar" nameLabel="Slideshow_property_hide_progressbar" type="boolean"/>
		<property name="Show slide" nameLabel="Slideshow_property_show_slide" type="string"/>
		<property name="Group next and previous buttons" nameLabel="Slideshow_property_group_next_and_previous_buttons" type="boolean"/>
		<property name="Narration" nameLabel="Slideshow_property_narration" type="narration"/>
		<property name="langAttribute" nameLabel="Slideshow_property_lang" type="string"/>
		<property name="Base width" nameLabel="Slideshow_Public_property_base_width" type="string"/>
		<property name="Base height" nameLabel="Slideshow_Public_property_base_height" type="string"/>
	</model>
<css>.slideshow-container {
    border: 1px solid black;
    margin: 0;
    padding: 0;
    position: relative;
    overflow: hidden;
}

.slideshow-container-slide {
    border: 0 solid black;
    margin: 0;
    padding: 0;
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    background-size: 100% 100%;
}

.slideshow-container-text {
    border: 0 solid black;
    margin: 0;
    padding: 0;
    opacity: 0;
    position: absolute;
    z-index: 100;
    color: white;
}

.slideshow-controls-container {
    height: 50px;
    border: 1px solid black;
    margin: 0;
    padding-left: 5px;
    padding-right: 5px;
}

.slideshow-controls-slide-counter,
.slideshow-controls-timer,
.slideshow-controls-play,
.slideshow-controls-play-mouse-hover,
.slideshow-controls-play-pause,
.slideshow-controls-play-pause-mouse-hover,
.slideshow-controls-stop,
.slideshow-controls-stop-mouse-hover,
.slideshow-controls-progressbar,
.slideshow-controls-previous,
.slideshow-controls-previous-mouse-hover,
.slideshow-controls-previous-inactive,
.slideshow-controls-previous-inactive-mouse-hover,
.slideshow-controls-next,
.slideshow-controls-next-mouse-hover,
.slideshow-controls-next-inactive,
.slideshow-controls-next-inactive-mouse-hover {
    height: 40px;
    line-height: 40px;
    border: 0 solid black;
    margin:5px;
    float: left;
    cursor: pointer;
    text-align: center;
}

.slideshow-controls-next-inactive,
.slideshow-controls-next-inactive-mouse-hover,
.slideshow-controls-previous-inactive,
.slideshow-controls-previous-inactive-mouse-hover {
    cursor: default;
    color: #C8C4C4;
}

.slideshow-controls-timer {
    cursor: default;
}

.slideshow-controls-timer-time,
.slideshow-controls-timer-duration,
.slideshow-controls-timer-separator {
    display: inline-block;
    float:left;
    max-height: 40px;
}

.slideshow-controls-timer-separator {
    margin:0 5px;
}

.slideshow-controls-progressbar {
    width: auto;
    cursor: default;
    position: relative;
}

.slideshow-controls-progressbar-slider {
    z-index: 100;
    width: 10px;
    height: 10px;
    border-radius: 4px;
    -webkit-border-radius: 4px;
    -moz-border-radius: 4px;
    margin-top: 14px;
    border: 2px solid black;
    background-color: white;
    position: absolute;
    top: 0;
    left: 0;
}

.slideshow-controls-progressbar-line {
    height: 2px;
    border: 1px solid black;
    background-color: black;
    margin-left: 1px;
    margin-right: 1px;
    margin-top: 19px;
    position: absolute;
    top: 0;
    left: 0;
}

.slideshow-loading-image,
.slideshow-loading-text {
    margin: 0;
    padding: 0;
    display: none;
    z-index: 100;
    position: absolute;
}

.slideshow-loading-image {
    border: 0 solid black;
    width: 80px;
    height: 80px;
}

.slideshow-loading-text {
    width: 250px;
    height: 40px;
    line-height: 40px;
    text-align: center;
    border-bottom: 0 solid black;
    border-left: 1px solid black;
    border-right: 1px solid black;
    border-top: 1px solid black;
    background-color: white;
    border-radius: 5px 5px 0 0;
}
</css><view>&lt;div class="slideshow-container"&gt;&lt;/div&gt;
&lt;div class="slideshow-controls-container"&gt;
    &lt;div class="slideshow-controls-play"&gt;Play&lt;/div&gt;
    &lt;div class="slideshow-controls-stop"&gt;Stop&lt;/div&gt;
    &lt;div class="slideshow-controls-timer"&gt;
        &lt;span class="slideshow-controls-timer-time"&gt;00:00&lt;/span&gt;
        &lt;span class="slideshow-controls-timer-separator"&gt;/&lt;/span&gt;
        &lt;span class="slideshow-controls-timer-duration"&gt;--:--&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class="slideshow-controls-progressbar"&gt;
        &lt;div class="slideshow-controls-progressbar-line"&gt;&lt;/div&gt;
        &lt;div class="slideshow-controls-progressbar-slider"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="slideshow-controls-slide-counter"&gt;1/10&lt;/div&gt;
    &lt;div class="slideshow-controls-previous"&gt;Previous&lt;/div&gt;
    &lt;div class="slideshow-controls-next"&gt;Next&lt;/div&gt;
&lt;/div&gt;
&lt;img src="" alt="Loading..." class="slideshow-loading-image"/&gt;
&lt;div class="slideshow-loading-text"&gt;Loading...&lt;/div&gt;
</view><preview>&lt;div class="slideshow-container"&gt;&lt;/div&gt;
&lt;div class="slideshow-controls-container"&gt;
    &lt;div class="slideshow-controls-play"&gt;Play&lt;/div&gt;
    &lt;div class="slideshow-controls-stop"&gt;Stop&lt;/div&gt;
    &lt;div class="slideshow-controls-timer"&gt;
        &lt;span class="slideshow-controls-timer-time"&gt;00:00&lt;/span&gt;
        &lt;span class="slideshow-controls-timer-separator"&gt;/&lt;/span&gt;
        &lt;span class="slideshow-controls-timer-duration"&gt;12:34&lt;/span&gt;
        &lt;span class="slideshow-controls-slide-counter"&gt;3/12&lt;/span&gt;
    &lt;/div&gt;
    &lt;div class="slideshow-controls-progressbar"&gt;
        &lt;div class="slideshow-controls-progressbar-line"&gt;&lt;/div&gt;
        &lt;div class="slideshow-controls-progressbar-slider"&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="slideshow-controls-previous"&gt;Previous&lt;/div&gt;
    &lt;div class="slideshow-controls-next"&gt;Next&lt;/div&gt;
&lt;/div&gt;
&lt;img src="/media/images/loading.gif" alt="Loading..." class="slideshow-loading-image"/&gt;
&lt;div class="slideshow-loading-text"&gt;Loading...&lt;/div&gt;
</preview><presenter>function AddonSlideshow_create() {

    var presenter = function() {};

    presenter.isPlaying = false;
    presenter.eventBus = null;
    presenter.noAudioPlayer = null;

    var isWCAGOn = false;
    presenter.isSpeaking = false;

    var DOMElements = {};
    var loadedImagesDeferred = $.Deferred(),
        loadedAudioDeferred = $.Deferred(),
        loadedTextDeferred = $.Deferred();

    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);

    presenter.configuration = {};

    presenter.ERROR_CODES = {
        'A_01': "At least one audio format file must be uploaded!",
        'A_02': "Your browser does not support HTML5 audio or none of Addon media types!",
        'A_03': "Your browser does not support MP3 media type. Please try to use file in OGG format.",
        'A_04': "Your browser does not support OGG media type. Please try to use file in MP3 format.",
        'S_01': "Each slide must have Image property set properly!",
        'S_02': "Slide start time in not in proper format ('MM:SS')!",
        'S_03': "Slide start times should be consecutive!",
        'T_01': "Text value cannot be empty!",
        'T_02': "Text start time in not in proper format ('MM:SS')!",
        'T_03': "Text end time in not in proper format ('MM:SS')!",
        'T_04': "Text end time must be higher than start time!",
        'T_05': "Top position value is invalid!",
        'T_06': "Left position value is invalid!",
        'T_07': "If more than one text is set, each one of them have to be set properly!",
        'N_01': "If 'no audio' is checked, presentation duration must be a positive number.",
        'ID_01': "Base width and height must be either positive integers or empty"
    };

    presenter.TIME_LINE_TASK = {
        TYPE:{
            SLIDE:0,
            TEXT:1
        },
        TASK:{
            SHOW:2,
            HIDE:3
        }
    };

    presenter.AUDIO_STATE = {
        PLAY: 0,
        PAUSE: 1,
        STOP: 2,
        NONE: 3,
        STOP_FROM_NAVIGATION: 4
    };

    presenter.checkBackgroundImageOfButtonElements = function(buttons) {
        var pattern = /url(.)/;
        $.each(buttons, function() {
            var background = $(this).css("background-image");
            var isBackgroundSet = pattern.test(background);
            if(isBackgroundSet) {
                $(this).html("");
            }
        });
    };

    function deferredQueueDecoratorChecker() {
        return presenter.configuration.audioLoadComplete;
    }

    function setElementsDimensions(width, height) {
        var container = DOMElements.viewContainer.find('.slideshow-container:first')[0];
        var containerDimensions = DOMOperationsUtils.getOuterDimensions(container);
        var containerDistances = DOMOperationsUtils.calculateOuterDistances(containerDimensions);

        var controlsContainer = DOMElements.viewContainer.find('.slideshow-controls-container:first')[0];
        var controlsContainerDimensions = DOMOperationsUtils.getOuterDimensions(controlsContainer);
        var controlsContainerDistances = DOMOperationsUtils.calculateOuterDistances(controlsContainerDimensions);

        presenter.configuration.slideDimensions = {
            width:width - containerDistances.horizontal,
            height:height - $(controlsContainer).height() - containerDistances.vertical - controlsContainerDistances.vertical
        };

        $(controlsContainer).css({
            width:width - controlsContainerDistances.horizontal
        });

        $(container).css({
            width:presenter.configuration.slideDimensions.width,
            height:presenter.configuration.slideDimensions.height
        });

        //adjust base dimensions
        if (presenter.configuration.baseDimensions.width &gt; 0) {
            presenter.configuration.baseDimensions.width -= containerDistances.horizontal;
        }
        if (presenter.configuration.baseDimensions.height &gt; 0) {
            presenter.configuration.baseDimensions.height -= ($(controlsContainer).height() + containerDistances.vertical + controlsContainerDistances.vertical);
        }
    }

    function adjustProgressBar() {
        var progressbar = $(DOMElements.controls.progressbar);

        if (presenter.configuration.hideProgressbar) {
            $(progressbar).hide();

            return;
        }

        var progressbarDimensions = DOMOperationsUtils.getOuterDimensions(progressbar);
        var progressbarDistances = DOMOperationsUtils.calculateOuterDistances(progressbarDimensions);

        var line = $(DOMElements.controls.line);
        var lineDimensions = DOMOperationsUtils.getOuterDimensions(line);
        var lineDistances = DOMOperationsUtils.calculateOuterDistances(lineDimensions);

        var controlsSpareWidth = $(DOMElements.controls.container).width();
        controlsSpareWidth -= $(DOMElements.controls.timer).outerWidth(true);
        controlsSpareWidth -= getControlButtonsDOMElements().play.outerWidth(true);
        controlsSpareWidth -= getControlButtonsDOMElements().stop.outerWidth(true);
        if (presenter.configuration.groupNextAndPrevious) {
            controlsSpareWidth -= $(getControlButtonsDOMElements().previous).parent().outerWidth(true);
        } else {
            controlsSpareWidth -= getControlButtonsDOMElements().previous.outerWidth(true);
            controlsSpareWidth -= getControlButtonsDOMElements().next.outerWidth(true);
        }
        controlsSpareWidth -= progressbarDistances.horizontal;

        var sliderOuterWidth = $(DOMElements.controls.slider).outerWidth(true);
        presenter.configuration.sliderSlideAreaLength = controlsSpareWidth - sliderOuterWidth;

        $(progressbar).css('width', controlsSpareWidth + 'px');
        $(line).css({
            width:(controlsSpareWidth - lineDistances.horizontal) + 'px'
        });
    }

    function updateProgressBar(time) {
        if (!presenter.configuration.buzzAudio) {
            return;
        }

        if (time === undefined) {
            time = presenter.configuration.buzzAudio.getTime();
        }
        var percentage = time / presenter.configuration.buzzAudio.getDuration();
        $(DOMElements.controls.slider).css('left', (percentage * presenter.configuration.sliderSlideAreaLength) + 'px');
    }

    function loadAudio(isPreview) {
        if (presenter.configuration.noAudio) {
            presenter.configuration.buzzAudio = new buzz.sound([]);
            presenter.configuration.audioLoadComplete = true;
            loadedAudioDeferred.resolve();
            return {isError: false};
        }

        if (!buzz.isSupported()) {
            return { isError: true, errorCode: "A_01" };
        }

        if (!buzz.isMP3Supported() &amp;&amp; !buzz.isOGGSupported()) {
            return { isError: true, errorCode: "A_02" };
        }

        if (presenter.configuration.audio.MP3 &amp;&amp; !buzz.isMP3Supported() &amp;&amp; buzz.isOGGSupported() &amp;&amp; !presenter.configuration.audio.OGG) {
            return { isError: true, errorCode: "A_03" };
        } else if (presenter.configuration.audio.OGG &amp;&amp; !buzz.isOGGSupported() &amp;&amp; buzz.isMP3Supported() &amp;&amp; !presenter.configuration.audio.MP3) {
            return { isError: true, errorCode: "A_04" };
        }

        if (!isPreview) {
            showLoadingScreen("Loading audio file...");
        }

        buzz.defaults.autoplay = false;
        buzz.defaults.loop = false;

        if (presenter.configuration.audio.MP3 &amp;&amp; buzz.isMP3Supported()) {
            presenter.configuration.buzzAudio = new buzz.sound([
                presenter.configuration.audio.MP3
            ]);
        } else if (presenter.configuration.audio.OGG &amp;&amp; buzz.isOGGSupported()) {
            presenter.configuration.buzzAudio = new buzz.sound([
                presenter.configuration.audio.OGG
            ]);
        }

        presenter.configuration.buzzAudio.bind("error", function () {
            var errorMessage = "Error occurred while loading/playing audio.";

            if (this.getErrorMessage()) {
                errorMessage += " Reason: " + this.getErrorMessage();
            }

            DOMElements.viewContainer.html(errorMessage + " Please try again.");
        });

        presenter.configuration.buzzAudio.bind("loadedmetadata", function () {
            var duration = buzz.toTimer(presenter.configuration.buzzAudio.getDuration(), false);
            presenter.configuration.audioDurationSet = presenter.configuration.buzzAudio.getDuration() !== '--';
            $(DOMElements.controls.duration).text(duration);
        });

        presenter.configuration.buzzAudio.bind("canplay", function () {
            presenter.configuration.audioLoadComplete = true;
            loadedAudioDeferred.resolve();
        });

        presenter.configuration.currentTime = 0;
        presenter.configuration.audioState = presenter.AUDIO_STATE.NONE;

        return {
            isError:false
        };
    }

    function updateSlideCounter(index) {
        var timeText = (index + 1) + '/' + presenter.configuration.slides.count;
        DOMElements.controls.counter.text(timeText);
    }

    function executeTasks(time, withoutAnimation) {
        var isTextAnimation = presenter.configuration.textAnimation &amp;&amp; !withoutAnimation;
        var isSlideAnimation = presenter.configuration.slideAnimation &amp;&amp; !withoutAnimation;

        if (presenter.configuration.timeLine[time]) {
            for (var i = 0; i &lt; presenter.configuration.timeLine[time].length; i++) {
                var type = presenter.configuration.timeLine[time][i].type;
                var index = presenter.configuration.timeLine[time][i].index;

                switch (type) {
                    case presenter.TIME_LINE_TASK.TYPE.SLIDE:
                        var showIndex = time !== 0 ? index : 0;

                        for (var j = 0; j &lt; presenter.configuration.slides.domReferences.length; j++) {
                            var $slideElement = $(presenter.configuration.slides.domReferences[j]);
                            if (j === showIndex) {
                                updateSlideCounter(j);
                                if (isSlideAnimation) {
                                    $slideElement.show("fade", {}, 2000);
                                } else {
                                    $slideElement.show();
                                }
                            } else {
                                if (isSlideAnimation) {
                                    $slideElement.hide("fade", {}, 2000);
                                } else {
                                    $slideElement.hide();
                                }
                            }
                        }
                        if (presenter.isPlaying) {
                            presenter.readSlide(index, true);
                        }
                        setButtonActive(presenter.NAVIGATION_BUTTON.PREVIOUS);
                        setButtonActive(presenter.NAVIGATION_BUTTON.NEXT);

                        if (showIndex === 0) { // first
                            setButtonInactive(presenter.NAVIGATION_BUTTON.PREVIOUS);
                        } else if (showIndex === presenter.configuration.slides.count-1) { // last
                            setButtonInactive(presenter.NAVIGATION_BUTTON.NEXT);
                        }

                        break;
                    case presenter.TIME_LINE_TASK.TYPE.TEXT:
                        var show = presenter.configuration.timeLine[time][i].task === presenter.TIME_LINE_TASK.TASK.SHOW,
                            $textElement = $(presenter.configuration.texts.domReferences[index]);
                        if (show) {
                            if (isTextAnimation) {
                                $textElement.css({'opacity': '1'}).effect('slide', {}, 500);
                            } else {
                                $textElement.css('opacity', '1');
                            }
                        } else {
                            if (isTextAnimation) {
                                $textElement.animate({'opacity': '0'}, {}, 500);
                            } else {
                                $textElement.css('opacity', '0');
                            }
                        }

                        break;
                }
            }
        }
    }

    presenter.pauseAudioResource = function () {
        presenter.isPlaying = false;
        if (presenter.configuration.noAudio) {
            presenter.pauseNoAudioPlayer();
        } else {
            if (presenter.configuration.audio.wasPlayed) {
                try {
                    presenter.configuration.buzzAudio.pause();
                } catch (exception) {
                }  //There can ba DOMException, if audio was player but was still buffering
            }
        }
    };

    presenter.playAudioResource = function () {
        presenter.isPlaying = true;
        if (presenter.configuration.noAudio) {
            presenter.configuration.audio.wasPlayed = true;
            presenter.startNoAudioPlayer();
        } else {
            presenter.configuration.audio.wasPlayed = true;
            var nopromise = {
                catch: new Function()
            };

            (presenter.configuration.buzzAudio.get().play() || nopromise).catch(function () {
            }); //There can ba DOMException, if audio was player but was still buffering
        }
    };

    presenter.startNoAudioPlayer = function() {
        presenter.noAudioPlayer = setInterval(noAudioPlay,1000);
    };

    presenter.pauseNoAudioPlayer = function() {
        clearInterval(presenter.noAudioPlayer);
        presenter.noAudioPlayer = null;
    };


    function noAudioPlay() {
        var time = presenter.time + 1;
        if (time &gt; presenter.configuration.maxTime) {
            onPresentationEnd();
            return;
        }
        executeTasks(time, false);
        presenter.configuration.currentTime = time;
        presenter.time = time;

    }

    function timeUpdateCallback() {
        if (presenter.configuration.audioState !== presenter.AUDIO_STATE.STOP) {
            updateProgressBar();
        }

        var parsedTime = parseInt(buzz.fromTimer(presenter.configuration.buzzAudio.getTime()), 10);
        var timeChanged = false;

        if (presenter.configuration.currentTime !== parsedTime) {
            timeChanged = true;
            var time = buzz.toTimer(presenter.configuration.buzzAudio.getTime(), false);
            presenter.configuration.currentTime = parsedTime;
            $(DOMElements.controls.currentTime).text(time);
        }

        if (presenter.configuration.buzzAudio.getTime() + 0.3 &gt; presenter.configuration.buzzAudio.getDuration()) {
            onPresentationEnd();
            return;
        }

        if (!timeChanged) {
            return; // We want to execute tasks with an accuracy of full seconds only
        }

        executeTasks(presenter.configuration.currentTime, false);
    }

    function onPresentationEnd() {
        presenter.time = 0;
        $(DOMElements.controls.currentTime).text('00:00');
        changeButtonToPlay();
        updateProgressBar(0);
        presenter.configuration.currentTime = 0;
        presenter.pauseAudioResource();
        if (presenter.configuration.audioState == presenter.AUDIO_STATE.STOP || presenter.configuration.noAudio) {
            presenter.sendValueChangedEvent("end");
        }
        presenter.configuration.audioState = presenter.AUDIO_STATE.STOP;
        hideAllTexts();
        // This action will trigger time update callback, but it's the only way to assure that pressing play after end/stop will trigger playing audio
        executeTasks(0, true);
    }

    function getContainerPadding() {
        var topOffset = parseInt(DOMElements.container.css('paddingTop'), 10);
        var bottomOffset = parseInt(DOMElements.container.css('paddingBottom'), 10);
        var leftOffset = parseInt(DOMElements.container.css('paddingLeft'), 10);
        var rightOffset = parseInt(DOMElements.container.css('paddingRight'), 10);

        return { topOffset:topOffset, bottomOffset:bottomOffset, leftOffset:leftOffset, rightOffset:rightOffset };
    }

    function loadSlides(width, height, isPreview) {
        showLoadingScreen("Loading slides...");
        var containerPadding = getContainerPadding();

        var images = [];
        for (var i = 0; i &lt; presenter.configuration.slides.count; i++) {
            images.push(presenter.configuration.slides.content[i].image);
        }

        $.imgpreload(images, {
            each:function () {
                var index = -1;

                for (var j = 0; j &lt; presenter.configuration.slides.count; j++) {
                    if (presenter.configuration.slides.content[j].image === $(this).attr('src')) {
                        index = j;
                        break;
                    }
                }

                var slide = document.createElement('div');

                $(slide).addClass('slideshow-container-slide');
                $(slide).css({
                    width: width + 'px',
                    height: height + 'px',
                    backgroundImage: 'url(' + $(this).attr('src') + ')',
                    top: containerPadding.topOffset + 'px',
                    left: containerPadding.leftOffset + 'px'
                });

                if (!presenter.configuration.slides.domReferences || !$.isArray(presenter.configuration.slides.domReferences)) {
                    presenter.configuration.slides.domReferences = [];
                }

                presenter.configuration.slides.domReferences[index] = slide;
            },
            all:function () {
                $(DOMElements.container).html();

                for (var i = 0; i &lt; presenter.configuration.slides.count; i++) {
                    $(DOMElements.container).append(presenter.configuration.slides.domReferences[i]);
                }

                presenter.configuration.isDomReferenceArrayComplete = true;
                $(presenter.configuration.slides.domReferences[0]).show();

                setButtonInactive(presenter.NAVIGATION_BUTTON.PREVIOUS);
                if (presenter.configuration.slides.count === 1) {
                    setButtonInactive(presenter.NAVIGATION_BUTTON.NEXT);
                }

                if (!isPreview) {
                    handleMouseActions();
                    handleMouseHovering();
                    presenter.configuration.buzzAudio.bind("timeupdate", timeUpdateCallback);
                }
                var slideNumber = isPreview ? presenter.configuration.showSlide - 1 : 0;
                presenter.goToSlide(slideNumber, true);
                loadedTextDeferred.resolve();

                if (presenter.configuration.savedState) {
                    $(DOMElements.viewContainer).trigger("onLoadSlidesEnd", [presenter.configuration.savedState]);
                }
                loadedImagesDeferred.resolve();
            }
        });

    }

    function loadTexts() {
        showLoadingScreen("Loading text labels...");

        if (!presenter.configuration.texts.domReferences || !$.isArray(presenter.configuration.texts.domReferences)) {
            presenter.configuration.texts.domReferences = [];
        }
        var scaleX = 1.0;
        var scaleY = 1.0;

        if (presenter.configuration.baseDimensions.width != 0) {
            scaleX = presenter.configuration.slideDimensions.width / presenter.configuration.baseDimensions.width;
        }
        if (presenter.configuration.baseDimensions.height != 0) {
            scaleY = presenter.configuration.slideDimensions.height / presenter.configuration.baseDimensions.height;
        }


        for (var i = 0; i &lt; presenter.configuration.texts.count; i++) {
            var text = presenter.configuration.texts.content[i];
            var textElement = document.createElement('span');
            $(textElement).addClass('slideshow-container-text');
            $(textElement).html(text.text);
            $(textElement).css({
                top:text.top * scaleY + 'px',
                left:text.left * scaleX + 'px'
            });
            if (scaleX != 1.0 || scaleY != 1.0) {
            $(textElement).css(generateTransformDict(scaleX,scaleY));
            }
            presenter.configuration.texts.domReferences[i] = textElement;
            $(DOMElements.container).append(textElement);
        }

        loadedTextDeferred.resolve();
    }

    function generateTransformDict(scaleX, scaleY) {
        var scale = "scale(" + scaleX + "," + scaleY + ")";
        return {
            'transform': scale,
            '-ms-transform': scale,
            '-webkit-transform': scale,
            '-o-transform': scale,
            '-moz-transform': scale,
           "-webkit-transform-origin": "top left",
           "-ms-transform-origin": "top left",
           "transform-origin": "top left"
        }
    }

    function hideAllTexts() {
        for (var i = 0; i &lt; presenter.configuration.texts.domReferences.length; i++) {
            $(presenter.configuration.texts.domReferences[i]).css('opacity', '0');
        }
    }

    function stopAllAnimations() {
        for (var i = 0; i &lt; presenter.configuration.texts.domReferences.length; i++) {
            $(presenter.configuration.texts.domReferences[i]).stop(true, true);
        }

        for (var i = 0; i &lt; presenter.configuration.slides.domReferences.length; i++) {
            $(presenter.configuration.slides.domReferences[i]).stop(true, true);
        }

        if (presenter.noAudioPlayer) {
            presenter.pauseNoAudioPlayer();
        }
    }

    presenter.stopPresentation = function() {
        $(DOMElements.controls.currentTime).text('00:00');
        updateProgressBar(0);
        presenter.configuration.audioState = presenter.AUDIO_STATE.STOP;
        presenter.sendValueChangedEvent("stop");
        presenter.pauseAudioResource();
        stopAllAnimations();
        hideAllTexts();
        executeTasks(0, true);
        changeButtonToPlay();
        presenter.time = 0;
    };

    // Returns currently displayed index. If none slide is visible then this function returns -1
    presenter.getCurrentSlideIndex = function () {
        stopAllAnimations();
        for (var i = 0; i &lt; presenter.configuration.slides.domReferences.length; i++) {
            if ($(presenter.configuration.slides.domReferences[i]).is(":visible")) {
                return i;
            }
        }

        return -1;
    };

    presenter.setTimeFromSlideIndex = function (slideIndex) {
        if (presenter.configuration.noAudio) {
            return;
        }
        var slide = presenter.configuration.slides.content[slideIndex];
        var time = slide.start;
        presenter.configuration.buzzAudio.setTime(time);
    };

    function goToNextSlide(withoutAnimation) {
        var currentSlideIndex = presenter.getCurrentSlideIndex();

        if (currentSlideIndex &lt; presenter.configuration.slides.count - 1) {
            var index = currentSlideIndex + 1;
            presenter.goToSlide(index, withoutAnimation);
            presenter.setTimeFromSlideIndex(index);
        }
    }

    function goToPreviousSlide(withoutAnimation) {
        var currentSlideIndex = presenter.getCurrentSlideIndex();

        if (currentSlideIndex &gt; 0) {
            var index = currentSlideIndex - 1;
            presenter.goToSlide(index, withoutAnimation);
            presenter.setTimeFromSlideIndex(index);
        }
    }

    presenter.goToSlide = function (index, withoutAnimation) {
        presenter.configuration.audioState = presenter.AUDIO_STATE.STOP_FROM_NAVIGATION;
        hideAllTexts();
        var slide = presenter.configuration.slides.content[index];
        var time = slide.start;
        executeTasks(time, withoutAnimation);
        $(DOMElements.controls.currentTime).text(buzz.toTimer(time, false));

        var activeTexts = presenter.findActiveTexts(presenter.configuration.timeLine, time);
        for (var i = 0; i &lt; activeTexts.length; i++) {
            var textIndex = activeTexts[i];
            $(presenter.configuration.texts.domReferences[textIndex]).css('opacity', '1');
        }

        updateProgressBar(time);
        presenter.configuration.currentTime = time;
        presenter.time = time;

        if (index &gt; 0) {
            setButtonActive(presenter.NAVIGATION_BUTTON.PREVIOUS);
        } else {
            setButtonInactive(presenter.NAVIGATION_BUTTON.PREVIOUS);
        }

        if (index &lt; presenter.configuration.slides.count - 1) {
            setButtonActive(presenter.NAVIGATION_BUTTON.NEXT);
        } else {
            setButtonInactive(presenter.NAVIGATION_BUTTON.NEXT);
        }
    };

    presenter.switchSlideShowStopToPlay = function () {
        updateProgressBar(0);
        presenter.configuration.currentTime = 0;
        if (presenter.configuration.audio.wasPlayed) {
            presenter.configuration.buzzAudio.set('currentTime', 0.1);
        }
        presenter.playAudioResource();
        presenter.configuration.audioState = presenter.AUDIO_STATE.PLAY;
        presenter.sendValueChangedEvent("playing");
        changeButtonToPause();
    };

     presenter.switchSlideShowPlayToPause = function () {
        presenter.pauseAudioResource();
        presenter.configuration.audioState = presenter.AUDIO_STATE.PAUSE;
        presenter.sendValueChangedEvent("pause");
        changeButtonToPlay();
    };

    presenter.switchSlideShowPauseToPlay = function () {
        presenter.configuration.audioState = presenter.AUDIO_STATE.PLAY;
        presenter.sendValueChangedEvent("playing");
        presenter.playAudioResource();
        changeButtonToPause();
    };

    presenter.switchSlideShowToPlay = function () {
        presenter.sendValueChangedEvent("playing");
        if (presenter.isPlaying) {
            presenter.pauseAudioResource();
            presenter.configuration.audioState = presenter.AUDIO_STATE.PAUSE;
            changeButtonToPlay();
        } else {
            presenter.playAudioAction();
        }
    };

    presenter.playAudioAction = function () {
        presenter.configuration.buzzAudio.setTime(presenter.time);
        updateProgressBar(presenter.time);
        presenter.configuration.currentTime = presenter.time;
        presenter.playAudioResource();
        changeButtonToPause();
    };

    var playButtonClickHandler = deferredSyncQueue.decorate(function playButtonClickHandler(event) {
        event.stopPropagation();

        switch (presenter.configuration.audioState) {
            case presenter.AUDIO_STATE.PLAY:
                presenter.switchSlideShowPlayToPause();
                break;
            case presenter.AUDIO_STATE.NONE:
            case presenter.AUDIO_STATE.PAUSE:
                presenter.switchSlideShowPauseToPlay();
                break;
            case presenter.AUDIO_STATE.STOP:
                presenter.switchSlideShowStopToPlay();
                break;
            case presenter.AUDIO_STATE.STOP_FROM_NAVIGATION:
                presenter.switchSlideShowToPlay();
                break;
        }
    });

    var stopButtonClickHandler = deferredSyncQueue.decorate(function stopButtonClickHandler(e) {
        e.stopPropagation();
        presenter.stopPresentation();
    });

    var previousButtonClickHandler = deferredSyncQueue.decorate(function previousButtonClickHandler(e) {

        e.stopPropagation();

        var isActive = $(this).hasClass('slideshow-controls-previous') || $(this).hasClass('slideshow-controls-previous-mouse-hover');
        if (isActive) {
            goToPreviousSlide(false);
            presenter.configuration.audioState = presenter.AUDIO_STATE.STOP_FROM_NAVIGATION;
        }
    });

    var nextButtonClickHandler = deferredSyncQueue.decorate(function nextButtonClickHandler(e) {
        e.stopPropagation();
        var isActive = $(this).hasClass('slideshow-controls-next') || $(this).hasClass('slideshow-controls-next-mouse-hover');
        if (isActive) {
            goToNextSlide(false);
            presenter.configuration.audioState = presenter.AUDIO_STATE.STOP_FROM_NAVIGATION;
        }
    });

    function getCurrentIndex(element) {
        return $(element).index() - presenter.configuration.texts.count;
    }

    function mouseDownCallback(eventData) {
        //if (presenter.AUDIO_STATE.PLAY === presenter.configuration.audioState) return;

        var currentIndex = getCurrentIndex(eventData.target), $slide;
        var containerPadding = getContainerPadding();
        presenter.configuration.mouseData.isMouseDown = true;
        presenter.configuration.mouseData.oldPosition.x = eventData.pageX;
        presenter.configuration.mouseData.oldPosition.y = eventData.pageY;
        presenter.configuration.mouseData.$imageElement = $(eventData.target);

        // Previous slide
        $slide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex - 1) + ')');
        $slide.css({
            left: (-1 * presenter.configuration.slideDimensions.width + containerPadding.leftOffset) + 'px',
            top: containerPadding.topOffset + 'px',
            display: 'block'
        });

        // Next slide
        $slide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex + 1) + ')');
        $slide.css({
            left:(presenter.configuration.slideDimensions.width + containerPadding.leftOffset) + 'px',
            top: containerPadding.topOffset + 'px',
            display:'block'
        });

    }

    function touchStartCallback(event) {
        event.preventDefault();
        event.stopPropagation();

        var touch = event.touches[0] || event.changedTouches[0];
        mouseDownCallback(touch);
    }

    function cleanMouseData() {
        presenter.configuration.mouseData = {
            isMouseDown:false,
            oldPosition:{
                x:0,
                y:0
            },
            isMouseDragged:false
        };
    }

    function restoreTextsPositions() {
        for (var i = 0; i &lt; presenter.configuration.texts.count; i++) {
            var text = presenter.configuration.texts.content[i];
            var textElement = presenter.configuration.texts.domReferences[i];
            $(textElement).css({
                top: text.top + 'px',
                left: text.left + 'px'
            });
        }
    }

    function mouseUpCallback () {
        //if (presenter.AUDIO_STATE.PLAY === presenter.configuration.audioState) return;

        presenter.configuration.mouseData.isMouseDown = false;
        var leftOffset = getContainerPadding().leftOffset,
            width = presenter.configuration.slideDimensions.width,
            left = parseInt(presenter.configuration.mouseData.$imageElement.css('left'), 10),
            isMoreThan30Percent = Math.abs(left - leftOffset) &gt; (width / 10) * 3,
            animationTime = (Math.abs(left - leftOffset) / width) * 750,
            currentIndex = getCurrentIndex(presenter.configuration.mouseData.$imageElement), $previousSlide, $nextSlide;

        $previousSlide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex - 1) + ')');
        $nextSlide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex + 1) + ')');

        function restoreCurrentView () {
            presenter.configuration.mouseData.$imageElement.animate({
                'left': leftOffset + 'px'
            }, animationTime, "linear", function () {
                cleanMouseData();
            });

            $previousSlide.animate(
                { 'left': (-1 * presenter.configuration.slideDimensions.width + leftOffset) + 'px' },
                animationTime, "linear",
                function () { $previousSlide.hide(); $(this).css('left', leftOffset); }
            );
            $nextSlide.animate(
                { 'left': (presenter.configuration.slideDimensions.width + leftOffset) + 'px' },
                animationTime, "linear",
                function () { $nextSlide.hide(); $(this).css('left', leftOffset); }
            );

        }

        if (isMoreThan30Percent) {
            if (left &lt; 0) {
                if (currentIndex + 1 &gt;= presenter.configuration.slides.count) {
                    restoreCurrentView();
                } else {
                    presenter.configuration.mouseData.$imageElement.animate({
                        'left': (-1 * width + leftOffset) + 'px'
                    }, animationTime, "linear", function () {
                        presenter.configuration.mouseData.$imageElement.hide();
                        presenter.configuration.mouseData.$imageElement.css('left', leftOffset + 'px');
                        presenter.goToSlide(currentIndex + 1, true);
                        presenter.setTimeFromSlideIndex(currentIndex + 1);
                        cleanMouseData();
                    });

                    $nextSlide.animate({
                        'left': leftOffset + 'px'
                    }, animationTime, "linear", function () {
                        $previousSlide.hide();
                        $previousSlide.css('left', leftOffset + 'px');
                    });
                }
            } else {
                if (currentIndex - 1 &lt; 0) {
                    presenter.configuration.mouseData.$imageElement.animate({
                        'left': leftOffset + 'px'
                    }, animationTime, "linear", function () {
                        cleanMouseData();
                    });
                    $previousSlide.animate(
                        { 'left': (-1 * presenter.configuration.slideDimensions.width + leftOffset) + 'px' },
                        animationTime, "linear",
                        function () { $previousSlide.hide(); }
                    );
                    $nextSlide.animate(
                        { 'left': (presenter.configuration.slideDimensions.width + leftOffset) + 'px' },
                        animationTime, "linear",
                        function () { $nextSlide.hide(); }
                    );
                } else {
                    presenter.configuration.mouseData.$imageElement.animate({
                        'left': (width + leftOffset) + 'px'
                    }, animationTime, "linear", function () {
                        presenter.configuration.mouseData.$imageElement.hide();
                        presenter.configuration.mouseData.$imageElement.css('left', leftOffset + 'px');
                        presenter.goToSlide(currentIndex - 1, true);
                        presenter.setTimeFromSlideIndex(currentIndex - 1);
                        cleanMouseData();
                    });

                    $previousSlide.animate({
                        'left': leftOffset + 'px'
                    }, animationTime, "linear", function () {
                        $nextSlide.hide();
                        $nextSlide.css('left', leftOffset + 'px');
                    });
                }
            }
        } else {
            restoreCurrentView();
        }
        restoreTextsPositions();
    }

    function touchEndCallback (event) {
        event.preventDefault();
        event.stopPropagation();

        mouseUpCallback();
    }

    function mouseMoveCallback (eventData) {
        //if (presenter.AUDIO_STATE.PLAY === presenter.configuration.audioState) return;
        if (presenter.configuration.mouseData.isMouseDown !== true) return;

        var imageElement = $(eventData.target);
        presenter.configuration.mouseData.isMouseDragged = true;
        var left = parseInt($(imageElement).css('left'), 10);
        var distance = eventData.pageX - presenter.configuration.mouseData.oldPosition.x;
        var currentIndex = getCurrentIndex(presenter.configuration.mouseData.$imageElement), $slide;

        presenter.configuration.mouseData.oldPosition.x = eventData.pageX;

        // Current slide
        $(imageElement).css('left', (left + distance) + 'px');

        // Previous slide
        $slide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex - 1) + ')');
        $slide.css('left', (parseInt($slide.css('left'), 10) + distance) + 'px');

        // Next slide
        $slide = DOMElements.container.find('.slideshow-container-slide:eq(' + (currentIndex + 1) + ')');
        $slide.css('left', (parseInt($slide.css('left'), 10) + distance) + 'px');

        DOMElements.container.find('.slideshow-container-text:visible').each(function () {
            var thisLeft = parseInt($(this).css('left'), 10);
            $(this).css('left', (thisLeft + distance) + 'px');
        });
    }

    function touchMoveCallback (event) {
        event.preventDefault();
        event.stopPropagation();

        var touch = event.touches[0] || event.changedTouches[0];
        mouseMoveCallback(touch);
    }

    function mouseClickCallback() {
        //if (presenter.AUDIO_STATE.PLAY === presenter.configuration.audioState) return;

        if (presenter.configuration.mouseData.isMouseDragged) {
            presenter.configuration.mouseData.isMouseDragged = false;
        }

        return false;
    }

    function handleMouseActions() {
        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            getControlButtonsDOMElements().play.on('touchend', playButtonClickHandler);
            getControlButtonsDOMElements().stop.on('touchend', stopButtonClickHandler);
            getControlButtonsDOMElements().previous.on('touchend', previousButtonClickHandler);
            getControlButtonsDOMElements().next.on('touchend', nextButtonClickHandler);
        } else {
            getControlButtonsDOMElements().play.click(playButtonClickHandler);
            getControlButtonsDOMElements().stop.click(stopButtonClickHandler);
            getControlButtonsDOMElements().previous.click(previousButtonClickHandler);
            getControlButtonsDOMElements().next.click(nextButtonClickHandler);
        }

        DOMElements.container.find('.slideshow-container-slide').each(function() {
            if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
                this.ontouchstart = touchStartCallback;
                this.ontouchend = touchEndCallback;
                this.ontouchmove = touchMoveCallback;
            } else {
                $(this).mousedown(mouseDownCallback);
                $(this).mouseup(mouseUpCallback);
                $(this).mousemove(mouseMoveCallback);
            }
        });

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            $(DOMElements.container.find('.slideshow-container-slide')).on('touchend', mouseClickCallback);
        } else {
            $(DOMElements.container.find('.slideshow-container-slide')).click(mouseClickCallback);
        }
    }

    function changeButtonToPlay() {
        var button = getControlButtonsDOMElements().play;
        var buttonClassName = $(button).attr('class');
        var isHover = buttonClassName.indexOf('-mouse-hover', buttonClassName.length - '-mouse-hover'.length) !== -1;

        $(button).attr('class', isHover ? 'slideshow-controls-play-mouse-hover' : 'slideshow-controls-play');
        $(button).text('Play');
        presenter.checkBackgroundImageOfButtonElements([button]);
        if (presenter.configuration.slides.count &lt; 2) {
            setButtonInactive(presenter.NAVIGATION_BUTTON.PREVIOUS);
            setButtonInactive(presenter.NAVIGATION_BUTTON.NEXT);
        } else {
            var currentSlideIndex = presenter.getCurrentSlideIndex();

            if (currentSlideIndex &lt; presenter.configuration.slides.count - 1) {
                setButtonActive(presenter.NAVIGATION_BUTTON.NEXT);
            } else {
                setButtonInactive(presenter.NAVIGATION_BUTTON.NEXT);
            }

            if (currentSlideIndex &gt; 0) {
                setButtonActive(presenter.NAVIGATION_BUTTON.PREVIOUS);
            } else {
                setButtonInactive(presenter.NAVIGATION_BUTTON.PREVIOUS);
            }
        }
    }

    function changeButtonToPause() {
        var button = getControlButtonsDOMElements().play;
        var buttonClassName = $(button).attr('class');
        var isHover = buttonClassName.indexOf('-mouse-hover', buttonClassName.length - '-mouse-hover'.length) !== -1;

        $(button).attr('class', isHover ? 'slideshow-controls-play-pause-mouse-hover' : 'slideshow-controls-play-pause');
        $(button).text('Pause');
        presenter.checkBackgroundImageOfButtonElements([button]);
    }

    presenter.NAVIGATION_BUTTON = {
        PREVIOUS:0,
        NEXT:1
    };

    function setButtonActive(whichButton) {
        var button = whichButton === presenter.NAVIGATION_BUTTON.PREVIOUS ? getControlButtonsDOMElements().previous : getControlButtonsDOMElements().next;
        var buttonClassName = $(button).attr('class');
        var isHover = buttonClassName.indexOf('-mouse-hover', buttonClassName.length - '-mouse-hover'.length) !== -1;

        if (whichButton === presenter.NAVIGATION_BUTTON.PREVIOUS) {
            $(button).attr('class', isHover ? 'slideshow-controls-previous-mouse-hover' : 'slideshow-controls-previous');
        } else {
            $(button).attr('class', isHover ? 'slideshow-controls-next-mouse-hover' : 'slideshow-controls-next');
        }
    }

    function setButtonInactive(whichButton) {
        var button = whichButton === presenter.NAVIGATION_BUTTON.PREVIOUS ? getControlButtonsDOMElements().previous : getControlButtonsDOMElements().next;
        var buttonClassName = $(button).attr('class');
        var isHover = buttonClassName.indexOf('-mouse-hover', buttonClassName.length - '-mouse-hover'.length) !== -1;

        if (whichButton === presenter.NAVIGATION_BUTTON.PREVIOUS) {
            $(button).attr('class', isHover ? 'slideshow-controls-previous-inactive-mouse-hover' : 'slideshow-controls-previous-inactive');
        } else {
            $(button).attr('class', isHover ? 'slideshow-controls-next-inactive-mouse-hover' : 'slideshow-controls-next-inactive');
        }
    }

    function handleMouseHovering() {
        getControlButtonsDOMElements().stop.hover(
            function () { // Hover in
                $(this).attr('class', 'slideshow-controls-stop-mouse-hover');
            },
            function () { // Hover out
                $(this).attr('class', 'slideshow-controls-stop');
            }
        );

        getControlButtonsDOMElements().play.hover(
            function () { // Hover in
                var isPlayButton = $(this).hasClass('slideshow-controls-play') || $(this).hasClass('slideshow-controls-play-mouse-hover');

                $(this).attr('class', isPlayButton ? 'slideshow-controls-play-mouse-hover' : 'slideshow-controls-play-pause-mouse-hover');
            },
            function () { // Hover out
                var isPlayButton = $(this).hasClass('slideshow-controls-play') || $(this).hasClass('slideshow-controls-play-mouse-hover');

                $(this).attr('class', isPlayButton ? 'slideshow-controls-play' : 'slideshow-controls-play-pause');
            }
        );

        getControlButtonsDOMElements().previous.hover(
            function () { // Hover in
                var isInactive = $(this).hasClass('slideshow-controls-previous-inactive') || $(this).hasClass('slideshow-controls-previous-inactive-mouse-hover');

                $(this).attr('class', isInactive ? 'slideshow-controls-previous-inactive-mouse-hover' : 'slideshow-controls-previous-mouse-hover');
            }, function () { // Hover out
                var isInactive = $(this).hasClass('slideshow-controls-previous-inactive') || $(this).hasClass('slideshow-controls-previous-inactive-mouse-hover');

                $(this).attr('class', isInactive ? 'slideshow-controls-previous-inactive' : 'slideshow-controls-previous');
            }
        );

        getControlButtonsDOMElements().next.hover(
            function () { // Hover in
                var isInactive = $(this).hasClass('slideshow-controls-next-inactive') || $(this).hasClass('slideshow-controls-next-inactive-mouse-hover');

                $(this).attr('class', isInactive ? 'slideshow-controls-next-inactive-mouse-hover' : 'slideshow-controls-next-mouse-hover');
            }, function () { // Hover out
                var isInactive = $(this).hasClass('slideshow-controls-next-inactive') || $(this).hasClass('slideshow-controls-next-inactive-mouse-hover');

                $(this).attr('class', isInactive ? 'slideshow-controls-next-inactive' : 'slideshow-controls-next');
            }
        );
    }

    // Finds active texts in time line for given time and returns an array of theirs indexes.
    // Active texts in some time is a task that has been displayed before given time and weren't hidden.
    presenter.findActiveTexts = function (timeLine, time) {
        var activeTasks = [];
        var i, j, type, index, show, indexOfElement;

        for (i = 0; i &lt; time; i++) {
            if (timeLine[i]) {
                for (j = 0; j &lt; timeLine[i].length; j++) {
                    type = timeLine[i][j].type;
                    index = timeLine[i][j].index;

                    if (type === presenter.TIME_LINE_TASK.TYPE.TEXT) {
                        show = timeLine[i][j].task === presenter.TIME_LINE_TASK.TASK.SHOW;

                        if (show) {
                            activeTasks.push(index);
                        } else {
                            indexOfElement = activeTasks.indexOf(index);
                            if (indexOfElement !== -1) {
                                activeTasks.splice(indexOfElement, 1);
                            }
                        }
                    }
                }
            }
        }

        // Removing tasks that end on given time
        if (timeLine[time]) {
            for (j = 0; j &lt; timeLine[time].length; j++) {
                type = timeLine[time][j].type;
                index = timeLine[time][j].index;

                if (type === presenter.TIME_LINE_TASK.TYPE.TEXT) {
                    show = timeLine[time][j].task === presenter.TIME_LINE_TASK.TASK.SHOW;

                    if (!show) {
                        indexOfElement = activeTasks.indexOf(index);
                        if (indexOfElement !== -1) {
                            activeTasks.splice(indexOfElement, 1);
                        }
                    }
                }
            }
        }

        return activeTasks;
    };

    presenter.buildTimeLine = function (slides, texts) {
        var timeLine = [];
        var i;

        // Slides
        for (i = 0; i &lt; slides.length; i++) {
            var time = slides[i].start;

            if (!timeLine[time]) {
                timeLine[time] = [];
            }

            timeLine[time].push({
                type:presenter.TIME_LINE_TASK.TYPE.SLIDE,
                task:presenter.TIME_LINE_TASK.TASK.SHOW,
                index:i
            });
        }

        for (i = 0; i &lt; texts.length; i++) {
            var startTime = texts[i].start;

            if (!timeLine[startTime]) {
                timeLine[startTime] = [];
            }

            timeLine[startTime].push({
                type:presenter.TIME_LINE_TASK.TYPE.TEXT,
                task:presenter.TIME_LINE_TASK.TASK.SHOW,
                index:i
            });

            var endTime = texts[i].end;

            if (!timeLine[endTime]) {
                timeLine[endTime] = [];
            }

            timeLine[endTime].push({
                type:presenter.TIME_LINE_TASK.TYPE.TEXT,
                task:presenter.TIME_LINE_TASK.TASK.HIDE,
                index:i
            });
        }

        return timeLine;
    };

    function setDOMElementsHrefsAndSelectors(view) {
        DOMElements.viewContainer = $(view);
        DOMElements.container = $(DOMElements.viewContainer.find('.slideshow-container:first')[0]);
        DOMElements.loading = {
            image:$(DOMElements.viewContainer.find('.slideshow-loading-image:first')[0]),
            text:$(DOMElements.viewContainer.find('.slideshow-loading-text:first')[0])
        };

        DOMElements.controls = {
            play:'[class*="slideshow-controls-play"]',
            pause:'[class*="slideshow-controls-pause"]',
            stop:'[class*="slideshow-controls-stop"]',
            next:'[class*=slideshow-controls-next]',
            previous:'[class*=slideshow-controls-previous]'
        };

        DOMElements.controls.container = $(DOMElements.viewContainer.find('.slideshow-controls-container:first')[0]);
        DOMElements.controls.timer = $(DOMElements.viewContainer.find('.slideshow-controls-timer:first')[0]);
        DOMElements.controls.currentTime = $(DOMElements.viewContainer.find('.slideshow-controls-timer-time:first')[0]);
        DOMElements.controls.duration = $(DOMElements.viewContainer.find('.slideshow-controls-timer-duration:first')[0]);
        DOMElements.controls.progressbar = $(DOMElements.viewContainer.find('.slideshow-controls-progressbar:first')[0]);
        DOMElements.controls.slider = $(DOMElements.viewContainer.find('.slideshow-controls-progressbar-slider:first')[0]);
        DOMElements.controls.line = $(DOMElements.viewContainer.find('.slideshow-controls-progressbar-line:first')[0]);
        DOMElements.controls.counter = $(DOMElements.viewContainer.find('.slideshow-controls-slide-counter:first')[0]);
    }

    function getControlButtonsDOMElements() {
        return {
            play:$(DOMElements.viewContainer.find(DOMElements.controls.play)[0]),
            pause:$(DOMElements.viewContainer.find(DOMElements.controls.pause)[0]),
            stop:$(DOMElements.viewContainer.find(DOMElements.controls.stop)[0]),
            next:$(DOMElements.viewContainer.find(DOMElements.controls.next)[0]),
            previous:$(DOMElements.viewContainer.find(DOMElements.controls.previous)[0])
        };
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = upgradeModelNoAudio(model);
        upgradedModel = upgradeModelAudiodescription(upgradedModel);
        upgradedModel = upgradeModelBaseDimensions(upgradedModel);
        return upgradedModel;
    };

    function upgradeModelNoAudio(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);
        if (!upgradedModel['No audio']) {
            upgradedModel['No audio'] = 'False'
        }
        return upgradedModel;
    }

    function upgradeModelAudiodescription(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);
        for (var i = 0; i &lt; upgradedModel.Slides.length; i++) {
            if (!upgradedModel.Slides[i]['Audiodescription']) {
                upgradedModel.Slides[i]['Audiodescription'] = '';
            }
        }
        if(!upgradedModel['langAttribute']) {
            upgradedModel['langAttribute'] = '';
        }
        return upgradedModel;
    }

    function upgradeModelBaseDimensions(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);
        if (!upgradedModel['Base width']) {
            upgradedModel['Base width'] = '';
        }
        if (!upgradedModel['Base height']) {
            upgradedModel['Base height'] = '';
        }
        return upgradedModel;
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
    };

    presenter.hideInactiveControls = function () {
      if (presenter.configuration.noAudio) {
            DOMElements.controls.timer.css('display','none');
            DOMElements.controls.progressbar.css('display','none');
      }  else {
          DOMElements.controls.counter.css('display','none');
      }

    };

    function presenterLogic(view, model, preview) {
        setDOMElementsHrefsAndSelectors(view);

        if (!preview) {
            var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
            if (loadingSrc) {
            	$(DOMElements.loading.image).attr('src', loadingSrc);
            }
        }
        presenter.$view = $(view);
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model, preview);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        if (!preview) {
            $.when(loadedAudioDeferred, loadedImagesDeferred, loadedTextDeferred).done(function () {
                hideLoadingScreen();
                deferredSyncQueue.resolve();
            });

        	if (presenter.configuration.groupNextAndPrevious) {
	            var $container = $(DOMElements.controls.container);
	            var $next = $(getControlButtonsDOMElements().next);
	            var $previous = $(getControlButtonsDOMElements().previous);
	            presenter.groupNavigationElements($container, $next, $previous);
	        }
	
	        setElementsDimensions(model.Width, model.Height);
	        adjustProgressBar();
	
	        var loadingResult = loadAudio(preview);
	        if (loadingResult.isError) {
	            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, loadingResult.errorCode);
	            return;
	        }
	
	        prepareLoadingScreen(presenter.configuration.slideDimensions.width, presenter.configuration.slideDimensions.height);
	        // Manual load is necessary for Apple iPad/iPhone
	        presenter.configuration.buzzAudio.load();
	        presenter.configuration.timeLine = presenter.buildTimeLine(presenter.configuration.slides.content, presenter.configuration.texts.content);
	        loadTexts();
	
	        var buttons = getControlButtonsDOMElements();
	        presenter.checkBackgroundImageOfButtonElements(buttons);
	        
	        loadSlides(presenter.configuration.slideDimensions.width, presenter.configuration.slideDimensions.height, preview);
	        
	        presenter.configuration.mouseData = {
	            isMouseDown : false,
	            oldPosition : {
	                x : 0,
	                y : 0
	            },
	            isMouseDragged : false
	        };

            MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.stopPresentation, presenter.$view.get(0));
            MutationObserverService.setObserver();

            presenter.hideInactiveControls();
        } else {
        	if (presenter.configuration.groupNextAndPrevious) {
	            var $container = $(DOMElements.controls.container);
	            var $next = $(getControlButtonsDOMElements().next);
	            var $previous = $(getControlButtonsDOMElements().previous);
	            presenter.groupNavigationElements($container, $next, $previous);
	        }
	
	        setElementsDimensions(model.Width, model.Height);
	        adjustProgressBar();
	        
	        prepareLoadingScreen(presenter.configuration.slideDimensions.width, presenter.configuration.slideDimensions.height);
	        // Manual load is necessary for Apple iPad/iPhone
	        presenter.configuration.timeLine = presenter.buildTimeLine(presenter.configuration.slides.content, presenter.configuration.texts.content);
	        loadTexts();
	
	        var buttons = getControlButtonsDOMElements();
	        presenter.checkBackgroundImageOfButtonElements(buttons);
	        
	        loadSlides(presenter.configuration.slideDimensions.width, presenter.configuration.slideDimensions.height, preview);
	        
	        presenter.configuration.mouseData = {
	            isMouseDown : false,
	            oldPosition : {
	                x : 0,
	                y : 0
	            },
	            isMouseDragged : false
	        };
            presenter.hideInactiveControls();
            hideLoadingScreen();
        }

    }

    function prepareLoadingScreen(slidesContainerWidth, slidesContainerHeight) {
        $(DOMElements.loading.image).css({
            top:((slidesContainerHeight - $(DOMElements.loading.image).height()) / 2) + 'px',
            left:((slidesContainerWidth - $(DOMElements.loading.image).width()) / 2) + 'px'
        });

        var textWidth = $(DOMElements.loading.text).width();
        var textHeight = $(DOMElements.loading.text).height();

        $(DOMElements.loading.text).css({
            top:(slidesContainerHeight - textHeight) + 'px',
            left:((slidesContainerWidth - textWidth) / 2) + 'px'
        });
    }

    function showLoadingScreen(text) {
        $(DOMElements.loading.image).show();
        $(DOMElements.loading.text).text(text);
        $(DOMElements.loading.text).show();
        presenter.configuration.isLoadingScreenVisible = true;
    }

    function hideLoadingScreen() {
        $(DOMElements.loading.image).hide();
        $(DOMElements.loading.text).hide();
        presenter.configuration.isLoadingScreenVisible = false;
    }

    presenter.groupNavigationElements = function($container, $next, $previous) {
        var $groupElement = $(document.createElement('div'));

        $groupElement.addClass('slideshow-controls-navigation');
        $groupElement.append($previous);
        $groupElement.append($next);

        $container.append($groupElement);
    };

    presenter.createPreview = function (view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function (view, model) {
        presenterLogic(view, model, false);
    };

    presenter.reset = function () {
        this.configuration.isVisible = this.configuration.isVisibleByDefault;
        this.setVisibility(this.configuration.isVisible);
        presenter.stopPresentation();
    };

    presenter.executeCommand = function(name, params) {

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'play': presenter.play,
            'pause': presenter.pause,
            'stop': presenter.stop,
            'next': presenter.next,
            'previous': presenter.previous,
            'moveTo': presenter.moveToCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.moveTo = function (number) {
        presenter.moveToCommand([number + ""]);
    };

    presenter.validateMoveToParams = function (params) {
        if (params.length &gt; 1 ) {
            return {isValid: false};
        }

        var number = Number(params[0]) - 1;

        if (isNaN(number)) {
            return {isValid: false};
        }

        if (number &gt; (presenter.configuration.slides.count - 1)) {
            return {isValid: false};
        }

        if (number &lt; 0) {
            return {isValid: false};
        }

        return {isValid: true, number: number};
    };

    presenter.moveToCommand = deferredSyncQueue.decorate(function (params) {
        var validatedParams = presenter.validateMoveToParams(params);

        if (validatedParams.isValid) {
            var previousAudioState = presenter.configuration.audioState;
            var wasPlayed = presenter.configuration.audio.wasPlayed;

            presenter.goToSlide(validatedParams.number, false);
            presenter.setTimeFromSlideIndex(validatedParams.number);

            presenter.onSlideChangeAudioStateSetting(previousAudioState, wasPlayed);
        }
    });

    presenter.onSlideChangeAudioStateSetting = function (previousAudioState, wasPlayed) {
        if (previousAudioState != presenter.AUDIO_STATE.PLAY) {
            presenter.configuration.audioState = presenter.AUDIO_STATE.PAUSE;
            presenter.sendValueChangedEvent("pause");
            presenter.configuration.audio.wasPlayed = wasPlayed;
            return;
        }

        presenter.configuration.audioState = presenter.AUDIO_STATE.PLAY;
        presenter.sendValueChangedEvent("playing");
        presenter.configuration.audio.wasPlayed = wasPlayed;
    };

    presenter.next = deferredSyncQueue.decorate(function () {
        var previousAudioState = presenter.configuration.audioState;
        var wasPlayed = presenter.configuration.audio.wasPlayed;

        goToNextSlide(false);

        presenter.onSlideChangeAudioStateSetting(previousAudioState, wasPlayed);
    });

    presenter.previous = deferredSyncQueue.decorate(function () {
        var previousAudioState = presenter.configuration.audioState;
        var wasPlayed = presenter.configuration.audio.wasPlayed;

        goToPreviousSlide(false);

        presenter.onSlideChangeAudioStateSetting(previousAudioState, wasPlayed);
    });

    presenter.play = deferredSyncQueue.decorate(function () {

        switch (presenter.configuration.audioState) {
            case presenter.AUDIO_STATE.STOP:
                presenter.switchSlideShowStopToPlay();
                presenter.readSlide(0, true);
                break;
            case presenter.AUDIO_STATE.PAUSE:
                presenter.switchSlideShowPauseToPlay();
                break;
            case presenter.AUDIO_STATE.STOP_FROM_NAVIGATION:
                presenter.sendValueChangedEvent("playing");
                if(!presenter.isPlaying) {
                    presenter.playAudioAction();
                    presenter.readSlide(0, true);
                }
                break;
        }
    });

    presenter.pause = deferredSyncQueue.decorate(function () {
        if(presenter.configuration.audioState == presenter.AUDIO_STATE.PLAY || presenter.isPlaying) {
            presenter.switchSlideShowPlayToPause();
        }
    });

    presenter.stop = deferredSyncQueue.decorate(function () {
        if(presenter.configuration.audioState != presenter.AUDIO_STATE.STOP) {
            presenter.stopPresentation();
        }
    });

    presenter.setVisibility = function(isVisible) {
        $(DOMElements.viewContainer).css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        if (presenter.configuration.audioState == presenter.AUDIO_STATE.PLAY) {
            presenter.playAudioResource();
        }
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        if (presenter.configuration.audioState == presenter.AUDIO_STATE.PLAY) {
            presenter.pauseAudioResource();
        }
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.onDestroy = function () {
        if (presenter.configuration.isDomReferenceArrayComplete) {
            presenter.pauseAudioResource();
        }
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        var isVisible = JSON.parse(state).isVisible;

        presenter.setVisibility(isVisible);
        presenter.configuration.isVisible = isVisible;
    };

    presenter.validateAudio = function (audioArray) {
        var audio = {
            MP3: audioArray.MP3 !== "" ? audioArray.MP3 : null,
            OGG: audioArray.OGG !== "" ? audioArray.OGG : null,
            wasPlayed: false
        };

        if (audio.MP3 === null &amp;&amp; audio.OGG === null) {
            return { isError: true, errorCode: "A_01" };
        }

        return { isError: false, audio: audio };
    };

    presenter.validateAnimation = function (slideAnimation, textAnimation) {
        return {
            textAnimation:textAnimation === 'True',
            slideAnimation:slideAnimation === 'True'
        };
    };

    function returnErrorObject() {
        return { isError: true }
    }

    // This function validates and converts timer in MM:SS format to number of seconds
    presenter.sanitizeTimer = function (timer) {
        if (!timer || timer.length === 0) {
            return returnErrorObject();
        }

        var buzzedTimer = buzz.fromTimer(timer);
        if (typeof buzzedTimer === "string") {
            if (buzzedTimer.split(':', 1).length !== 2) {
                return returnErrorObject();
            }

            buzzedTimer = parseInt(buzzedTimer, 10);
            if (isNaN(buzzedTimer) || buzzedTimer &lt; 0) {
                return returnErrorObject();
            }
        }

        if (buzzedTimer &lt; 0) {
            return returnErrorObject();
        }

        return {
            isError:false,
            sinitizedTimer:buzzedTimer
        };
    };

    // If validation error occurs then one of the following error codes are returned
    // S_01 - empty image
    // S_02 - problem with start time
    // S_03 - times imposed
    presenter.validateSlides = function (slidesArray) {
        var slides = {
            count:0,
            content:[]
        };

        for (var i = 0; i &lt; slidesArray.length; i++) {
            if (!slidesArray[i].Image || slidesArray[i].Image === "") {
                return {
                    isError:true,
                    errorCode:"S_01"
                };
            }

            var startTime = slidesArray[i].Start;
            var sanitizedTime = presenter.sanitizeTimer(startTime);

            if (sanitizedTime.isError) {
                return {
                    isError:true,
                    errorCode:"S_02"
                };
            }

            if (i === 0 &amp;&amp; sanitizedTime.sinitizedTimer !== 0) {
                sanitizedTime.sinitizedTimer = 0;
            }

            var slide = {
                image:slidesArray[i].Image,
                start:sanitizedTime.sinitizedTimer,
                audiodescription:slidesArray[i].Audiodescription
            };

            slides.content.push(slide);
            slides.count++;
        }

        var previousTime = slides.content[0].start;
        for (var s = 1; s &lt; slides.count; s++) {
            var nextTime = slides.content[s].start;
            if (nextTime &lt; previousTime) {
                return {
                    isError:true,
                    errorCode:"S_03"
                };
            }
            previousTime = nextTime;
        }

        return {
            isError:false,
            slides:slides
        };
    };

    // This function validates and converts number from string representation to integer value
    presenter.sanitizePosition = function (position) {
        if (!position) {
            return {
                isError:true
            };
        }

        var parsedPosition = parseInt(position, 10);
        if (isNaN(parsedPosition)) {
            return {
                isError:true
            };
        }

        if (parsedPosition &lt; 0) {
            return {
                isError:true
            };
        }

        return {
            isError:false,
            position:parseInt(position, 10)
        };
    };

    // If validation error occurs then one of the following error codes are returned
    // T_01 - empty image
    // T_02 - problem with start time
    // T_03 - problem with end time
    // T_04 - times imposed
    // T_05 - top value invalid
    // T_06 - left value invalid
    // T_07 - empty text and texts count &gt; 0
    presenter.validateTexts = function (textsArray) {
        var texts = {
            count:0,
            content:[]
        };

        for (var i = 0; i &lt; textsArray.length; i++) {
            var startTime = textsArray[i].Start;
            var endTime = textsArray[i].End;
            var topPosition = textsArray[i].Top;
            var leftPosition = textsArray[i].Left;

            if (textsArray[i].Text == "" &amp;&amp; startTime == "" &amp;&amp; endTime == "" &amp;&amp; topPosition == "" &amp;&amp; leftPosition == "") {
                if (textsArray.length == 1) {
                    texts.content = [];
                    texts.count = 0;
                    break;
                } else {
                    return {
                        isError:true,
                        errorCode:"T_07"
                    };
                }
            }

            if (!textsArray[i].Text || textsArray[i].Text === "") {
                return {
                    isError:true,
                    errorCode:"T_01"
                };
            }

            var sanitizedStartTime = presenter.sanitizeTimer(startTime);
            if (sanitizedStartTime.isError) {
                return {
                    isError:true,
                    errorCode:"T_02"
                };
            }

            var sanitizedEndTime = presenter.sanitizeTimer(endTime);
            if (sanitizedEndTime.isError) {
                return {
                    isError:true,
                    errorCode:"T_03"
                };
            }

            if (sanitizedStartTime.sinitizedTimer &gt; sanitizedEndTime.sinitizedTimer) {
                return {
                    isError:true,
                    errorCode:"T_04"
                };
            }

            var sanitizedTopPosition = presenter.sanitizePosition(topPosition);
            if (sanitizedTopPosition.isError) {
                return {
                    isError:true,
                    errorCode:"T_05"
                };
            }

            var sanitizedLeftPosition = presenter.sanitizePosition(leftPosition);
            if (sanitizedLeftPosition.isError) {
                return {
                    isError:true,
                    errorCode:"T_06"
                };
            }

            var text = {
                text:textsArray[i].Text,
                start:sanitizedStartTime.sinitizedTimer,
                end:sanitizedEndTime.sinitizedTimer,
                top:sanitizedTopPosition.position,
                left:sanitizedLeftPosition.position
            };

            texts.content.push(text);
            texts.count++;
        }

        return {
            isError:false,
            texts:texts
        };
    };

    /**
     * Validates string representation of integer. Only positive integer values are allowed. If both (value and default) are
     * undefined then isError property is set to true.
     *
     */
    presenter.validatePositiveInteger = function (value, defaultValue) {
        var isValueDefined = value != undefined &amp;&amp; value !== "";
        var isDefaultDefined = defaultValue != undefined &amp;&amp; !isNaN(defaultValue);

        if (!isValueDefined &amp;&amp; !isDefaultDefined) {
            return {
                isError: true
            };
        }

        if (!isValueDefined &amp;&amp; isDefaultDefined) {
            return {
                isError: false,
                value: defaultValue
            };
        }

        var parsedSize = parseInt(value, 10);
        if (isNaN(parsedSize) || parsedSize &lt; 1) {
            return {
                isError: true
            };
        }

        return {
            isError: false,
            value: parsedSize
        };
    };

    presenter.validateModel = function (model, isPreview) {
        var noAudio = ModelValidationUtils.validateBoolean(model["No audio"]);
		var animationValidationResult = presenter.validateAnimation(model["Slide animation"], model["Text animation"]);

		var audioValidationResult;
		if (noAudio) {
		    audioValidationResult = {audio: {wasPlayed: false}};
        } else {
            audioValidationResult = presenter.validateAudio(model.Audio[0]);
            if (audioValidationResult.isError) {
                return {
                    isError: true,
                    errorCode: audioValidationResult.errorCode
                };
            }
        }

        var slidesValidationResult = presenter.validateSlides(model.Slides);
        if (slidesValidationResult.isError) {
            return {
                isError:true,
                errorCode:slidesValidationResult.errorCode
            };
        }

        var textsValidationResult = presenter.validateTexts(model.Texts);
        if (textsValidationResult.isError) {
            return {
                isError:true,
                errorCode:textsValidationResult.errorCode
            };
        }

        var maxDurationResult = ModelValidationUtils.validateInteger(model['Presentation duration']);
        if (noAudio &amp;&amp; (!maxDurationResult.isValid || maxDurationResult.value &lt;= 0)) {
            return {isError: true, errorCode: 'N_01'};
        }

        var showSlide = 1;
        var validatedShowSlide = presenter.validatePositiveInteger(model["Show slide"], 1);
        if (!validatedShowSlide.isError &amp;&amp; validatedShowSlide.value &lt;= slidesValidationResult.slides.count) {
            showSlide = validatedShowSlide.value;
        }

        var isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        var baseDimensions = {
            width: 0,
            height: 0
        }
        if (model['Base width'].trim().length !== 0) {
            var validatedBaseWidth = ModelValidationUtils.validatePositiveInteger(model['Base width']);
            if (validatedBaseWidth.isValid) {
                baseDimensions.width = validatedBaseWidth.value;
            } else {
                return {isError: true, errorCode: 'ID_01'};
            }
        }
        if (model['Base height'].trim().length !== 0) {
            var validatedBaseHeight = ModelValidationUtils.validatePositiveInteger(model['Base height']);
            if (validatedBaseHeight.isValid) {
                baseDimensions.height = validatedBaseHeight.value;
            } else {
                return {isError: true, errorCode: 'ID_01'};
            }
        }

        return {
            isError: false,
            audio: audioValidationResult.audio,
            textAnimation: animationValidationResult.textAnimation,
            slideAnimation: animationValidationResult.slideAnimation,
            slides: slidesValidationResult.slides,
            texts: textsValidationResult.texts,
            hideProgressbar: ModelValidationUtils.validateBoolean(model["Hide progressbar"]),
            groupNextAndPrevious: ModelValidationUtils.validateBoolean(model["Group next and previous buttons"]),
            showSlide: showSlide,
            isVisibleByDefault: isVisibleByDefault,
            isVisible: isVisibleByDefault,
            addonID: model['ID'],
            noAudio: noAudio,
            maxTime: maxDurationResult.value,
            lang: model['langAttribute'],
            baseDimensions: baseDimensions
        };
    };

    presenter.sendValueChangedEvent = function slideShowAddon_sendValueChangedEvent (eventValue) {
        presenter.sendEvent({
            'source': presenter.configuration.addonID,
            'item': '',
            'value': eventValue,
            'score': ''
        }, 'ValueChanged');
    };

    presenter.sendEvent = function slideShowAddon_sendEvent(eventData, eventType) {
        if (presenter.eventBus != null) {
            presenter.eventBus.sendEvent(eventType, eventData);
        }
    };

    presenter._internal_state = {
        deferredQueue: deferredSyncQueue
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        event.preventDefault();
        presenter.shiftPressed = event.shiftKey;

        var keys = {
            ENTER: 13,
            ESC: 27,
            SPACE: 32,
            ARROW_LEFT: 37,
            ARROW_UP: 38,
            ARROW_RIGHT: 39,
            ARROW_DOWN: 40
        };

        var leftArrowHandler = function () {
            presenter.previous();

            presenter.readSlide(presenter.getCurrentSlideIndex(), false);
        };

        var rightArrowHandler = function () {
            presenter.next();
            presenter.readSlide(presenter.getCurrentSlideIndex(), false);
        };

        var spaceHandler = function () {
            if (presenter.isPlaying) {
                presenter.pause();
            } else {
                if (presenter.isSpeaking) {
                    presenter.stopSpeech();
                } else {
                    presenter.play();
                }
            }
        };

        var upArrowHandler = function () {
            if (!presenter.noAudioPlayer &amp;&amp; presenter.configuration.audioLoadComplete) {
                var volume = presenter.configuration.buzzAudio.getVolume();
                volume += 10;
                if (volume &gt; 100) {
                    volume = 100;
                }
                presenter.configuration.buzzAudio.setVolume(volume);
            }
        };

        var downArrowHandler = function () {
            if (!presenter.noAudioPlayer &amp;&amp; presenter.configuration.audioLoadComplete) {
                var volume = presenter.configuration.buzzAudio.getVolume();
                volume -= 10;
                if (volume &lt; 0) {
                    volume = 0;
                }
                presenter.configuration.buzzAudio.setVolume(volume);
            }
        };

        var escapeHandler = function () {
            presenter.pause();
            presenter.stopSpeech();
        };

        var enterHandler = function () {
            if (!isWCAGOn) {
                presenter.pause();
                presenter.stopSpeech();
            }
        };

        var mapping = {};

        mapping[keys.ENTER] = enterHandler;
        mapping[keys.ESC] = escapeHandler;
        mapping[keys.SPACE] = spaceHandler;
        mapping[keys.ARROW_LEFT] = leftArrowHandler;
        mapping[keys.ARROW_UP] = upArrowHandler;
        mapping[keys.ARROW_RIGHT] = rightArrowHandler;
        mapping[keys.ARROW_DOWN] = downArrowHandler;

        try {
            mapping[keycode]();
        } catch (er) {};
    };

    presenter.readSlide = function(index, continueAfterTTS) {
        var audiodescription = presenter.configuration.slides.content[index].audiodescription;
        if (isWCAGOn &amp;&amp; audiodescription.length &gt; 0) {
            presenter.pause();
            if (continueAfterTTS) {
                presenter.speakWithCallback(
                    [window.TTSUtils.getTextVoiceObject(
                        audiodescription,
                        presenter.configuration.lang)],
                    readSlideCallback);
            } else {
                presenter.speak([window.TTSUtils.getTextVoiceObject(
                        audiodescription,
                        presenter.configuration.lang)]);
            }
        }
    };

    function readSlideCallback() {
        if (presenter.isSpeaking) {
            presenter.isSpeaking = false;
            presenter.play();
        }
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.speakWithCallback = function(data, callback) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            presenter.isSpeaking = true;
            tts.speakWithCallback(data, callback);
        }
    };

    presenter.speak = function(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            presenter.isSpeaking = false;
            tts.speak(data);
        }
    };

    presenter.stopSpeech = function() {
         var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            presenter.isSpeaking = false;
            tts.speakWithCallback([window.TTSUtils.getTextVoiceObject("-")], presenter.pause);
        }
    };

    presenter.isEnterable = function() {
        presenter.pause();
        presenter.stopSpeech();
        return false;
    };

    return presenter;
}

AddonSlideshow_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Speechace" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="CourseId" nameLabel="Speechace_course_id" type="string"/>
	</model>
<css>.speechace-iframe {
    width: 100%;
    height: 100%;
    border: none;
    position: absolute;
}

.speechace-message {
    display: none;
    position: absolute;
    justify-content: center;
    align-items: center;
    z-index: 9999;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.6);
}

.speechace-message-text {
    color: white;
    font-size: 30px;
}
</css><view>&lt;div class="speechace-container"&gt;
    &lt;div class="speechace-message"&gt;
        &lt;div class="speechace-message-text"&gt;Fetching score...Don't leave page&lt;/div&gt;
    &lt;/div&gt;
    &lt;iframe class="speechace-iframe"&gt;
    &lt;/iframe&gt;
&lt;/div&gt;
</view><preview>&lt;div class="speechace-container"&gt;
    &lt;iframe class="speechace-iframe"&gt;
    &lt;/iframe&gt;
&lt;/div&gt;
</preview><presenter>function AddonSpeechace_create() {
    var presenter = function (){};

    var errorCodes = {
        "V_01": "Course ID is missing",
        "V_02": "Error occurred while fetching data",
        "V_03": "Missing configuration params in context metadata",
        "V_04": "Cannot load context metadata - setting default URLs"
    };

    presenter.eventBus = null;

    presenter.DEFAULTS = {
        JWTSessionTokenURL: "/api/v2/jwt/session_token",
        speechaceCourseURL: "/speechace/url/",
        collectionId: 0
    };

    presenter.JWTSessionTokenURL = "";
    presenter.speechaceCourseURL = "";
    presenter.isFetchingScore = false;

    presenter.presenterLogic = function AddonSpeechace_presenterLogic (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.view = view;
        presenter.isPreview = isPreview;

        const upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            presenter.createErrorView(presenter.configuration.errorCode);
            return;
        }

        presenter.setIframe(view);

        if (!presenter.isPreview) {
            const iterations = presenter.isMauthor() ? 1 : 10;
            presenter.runLogic(iterations);
            presenter.registerEvents();
        }
    };

    presenter.isMauthor = function AddonSpeechace_isMauthor () {
        const names = ["lorepo", "mauthor"];
        const origin = window.origin;
        return names.some((name) =&gt; origin.includes(name));
    };

    presenter.run = function AddonSpeechace_run (view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function AddonSpeechace_createPreview (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.runLogic = function AddonSpeechace_runLogic (iterationsLeft) {
        const context = presenter.playerController &amp;&amp; presenter.playerController.getContextMetadata();
        if (context != null) {
            if ("JWTSessionTokenURL" in context) {
                presenter.JWTSessionTokenURL = context["JWTSessionTokenURL"];
            }

            if ("speechaceCourseURL" in context) {
                presenter.speechaceCourseURL = context["speechaceCourseURL"];
            }

            presenter.collectionID = "collectionID" in context ? context["collectionID"] : presenter.DEFAULTS.collectionId;

            if (presenter.JWTSessionTokenURL.length &amp;&amp; presenter.speechaceCourseURL.length) {
                presenter.handleURLLogic();
            } else {
                presenter.createErrorView("V_03");
            }
        } else {
            if (iterationsLeft &gt; 0) {
                window.setTimeout(function(){presenter.runLogic(iterationsLeft-1)}, 500);
            } else {
                console.warn(errorCodes["V_04"]);
                console.warn("Setting default URLs - no context metadata was set");
                presenter.JWTSessionTokenURL = presenter.DEFAULTS.JWTSessionTokenURL;
                presenter.speechaceCourseURL = presenter.DEFAULTS.speechaceCourseURL;
                presenter.collectionID = presenter.DEFAULTS.collectionId;
                presenter.handleURLLogic();
            }
        }
    };

    presenter.setVisibility = function AddonSpeechace_setVisibility (isVisible) {
        presenter.$view.css("display", isVisible ? "" : "none");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.upgradeModel = function AddonSpeechace_upgradeModel (model) {
        return presenter.upgradeCourseId(model);
    };

    presenter.upgradeAttribute = function AddonSpeechace_upgradeAttribute (model, attrName, defaultValue) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (model[attrName] === undefined) {
            upgradedModel[attrName] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.upgradeCourseId = function AddonSpeechace_upgradeCourseId (model) {
        return presenter.upgradeAttribute(model, "CourseId", "");
    };

    presenter.validateModel = function AddonSpeechace_validateModel (model) {
        const validatedCourseId = presenter.validateCourseId(model["CourseId"]);
        if (!validatedCourseId.isValid) {
            return {isValid: false, errorCode: validatedCourseId.errorCode};
        }

        return {
            addonID: model.ID,
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            courseId: validatedCourseId.value,
            maxScore: 100
        };
    };

    presenter.validateCourseId = function AddonSpeechace_validateCourseId (courseId) {
        if (!courseId || !courseId.trim()) {
            return {isValid: false, errorCode: 'V_01'};
        }

        return {isValid: true, value: courseId.trim()};
    };

    presenter.createErrorView = function AddonSpeechace_createErrorView (errorCode) {
        presenter.$view.html(errorCodes[errorCode]);
    };

    presenter.setPlayerController = function AddonSpeechace_setPlayerController (controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.setIframe = function AddonSpeechace_setIframe (view) {
        presenter.iframe = $(view).find("iframe");
        presenter.iframe.attr("allow","microphone");
    };

    presenter.executeCommand = function AddonSpeechace_executeCommand (name, params) {
        if (!presenter.configuration.isValid) return;

        const commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.handleURLLogic = function AddonSpeechace_handleURLLogic () {
        presenter.fetchSessionJWTToken()
            .then((response) =&gt; response.json())
            .then((data) =&gt; presenter.getCourseURL(data.token))
            .then((response) =&gt; response.json())
            .then((data) =&gt; presenter.handleDataReceived(data))
            .catch((error) =&gt; {
                presenter.createErrorView("V_02");
                console.error(`Error fetching data: ${error}`)
            });
    };

    presenter.fetchSessionJWTToken = function() {
        return fetch(presenter.JWTSessionTokenURL, {method: 'GET'});
    };

    presenter.getCourseURL = function AddonSpeechace_getCourseURL (token) {
        let url = `${presenter.speechaceCourseURL}?course_key=${presenter.configuration.courseId}`;
        if (presenter.collectionID) {
            url += `&amp;collection_id=${presenter.collectionID}`;
        }
        const config = {
            method: 'GET',
            headers: { 'Authorization': `JWT ${token}` }
        };

        return fetch(url, config);
    };

    presenter.handleDataReceived = function AddonSpeechace_handleDataReceived (data) {
        presenter.iframe.attr("src", data.course_url);
        presenter.speechaceToken = data.token;
        presenter.speechaceUrl = data.speechace_url;
    };

    presenter.registerEvents = function AddonSpeechace_registerEvents () {
        window.addEventListener("message", presenter.handleMessageReceived, false);
        MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.handleMessageReceived = function AddonSpeechace_handleMessageReceived (event) {
        if (event.data === "speechaceActivityComplete") {
            presenter.requestScore();
        }
    };

    presenter.destroy = function AddonSpeechace_destroy (event) {
        if (event.target !== presenter.view) {
            return;
        }

        window.removeEventListener("message", presenter.handleMessageReceived);
    };

    presenter.getState = function AddonSpeechace_getState () {
        if (!presenter.configuration.isValid) return "";

        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            averageScore: presenter.averageScore
        });
    };

    presenter.setState = function AddonSpeechace_setState (state) {
        if (!state) return;

        const parsedState = JSON.parse(state);

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);

        presenter.updateScore(parsedState.averageScore);
    };

    presenter.showMessageDialog = function AddonSpeechace_showMessageDialog () {
        const messageElement = presenter.$view.find(".speechace-message");
        messageElement.css("display", "flex");
    };

    presenter.hideMessageDialog = function AddonSpeechace_hideMessageDialog () {
        const messageElement = presenter.$view.find(".speechace-message");
        messageElement.css("display", "none");
    };

    presenter.requestScore = function AddonSpeechace_requestScore() {
        presenter.isFetchingScore = true;
        window.setTimeout(function(){
            if (presenter.isFetchingScore) {
                presenter.showMessageDialog();
            }}, 1000);

        const url = presenter.generateScoreUrl();
        fetch(url, {method: 'GET'})
            .then((response) =&gt; response.json())
            .then((data) =&gt; presenter.saveScore(data))
            .catch((error) =&gt; {
                console.error(`Error fetching data: ${error}`);
                presenter.isFetchingScore = false;
                presenter.hideMessageDialog();
                presenter.onScoreFetchError();
            });
    };

    presenter.generateScoreUrl = function AddonSpeechace_generateScoreUrl() {
        const unixTimestamp = Math.round((new Date()).getTime() / 1000);
        const coreUrl = presenter.speechaceUrl;
        const courseId = presenter.configuration.courseId;
        const token = presenter.speechaceToken;

        return `${coreUrl}/embed/api/${courseId}/report?speechace_token=${token}&amp;session_start_time=${unixTimestamp}`;
    };

    presenter.saveScore = function AddonSpeechace_saveScore(data) {
        const status = data["status"];
        if (status !== "success") {
            console.error("Fetching score failed - status: error");
            console.error("Detailed message:");
            console.error(data["detail_message"]);
            presenter.onScoreFetchError();
            return;
        }

        let score = parseInt(data["report"]["averageScore"], 10);
        score = isNaN(score) ? 0 : score;
        presenter.updateScore(score);

        presenter.isFetchingScore = false;
        presenter.hideMessageDialog();
    };

    presenter.updateScore = function AddonSpeechace_updateScore (score) {
        presenter.averageScore = score;
        presenter.sendScoreUpdateEvent();
    };

    presenter.onScoreFetchError = function AddonSpeechace_onScoreFetchError () {
        const messageElement = presenter.$view.find(".speechace-message-text")[0];
        const prevMessage = messageElement.innerHTML;
        messageElement.innerHTML = "Error saving the score. Try again.";

        presenter.showMessageDialog();

        window.setTimeout(function(){
            presenter.hideMessageDialog();
            messageElement.innerHTML = prevMessage;
            }, 5000);
    };

    presenter.getMaxScore = function AddonSpeechace_getMaxScore () {
        return presenter.configuration.maxScore;
    };

    presenter.getScore = function AddonSpeechace_getScore () {
        return presenter.averageScore || 0;
    };

    presenter.sendScoreUpdateEvent = function AddonSpeechace_sendValueChangedEvent () {
        presenter.eventBus.sendEvent('ValueChanged', presenter.createEventData());
    };

    presenter.createEventData = function AddonSpeechace_createEventData () {
        return {
            'source' : presenter.configuration.addonID,
            'item' : '',
            'value' : '',
            'score' : presenter.averageScore || 0
        };
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Standard_Shapes" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Shape" nameLabel="Standard_Shapes_property_shape" type="{Line, Square, Rectangle, Circle, Ellipse}"/>
		<property name="Rotation angle" nameLabel="Standard_Shapes_property_rotation_angle" type="string"/>
		<property name="Stroke width" nameLabel="Standard_Shapes_property_stroke_width" type="string"/>
		<property name="Stroke color" nameLabel="Standard_Shapes_property_stroke_color" type="string"/>
		<property name="Corners rounding" nameLabel="Standard_Shapes_property_corners_rounding" type="boolean"/>
		<property name="Line ending" nameLabel="Standard_Shapes_property_line_ending" type="{None, Circles, Arrows, None - Arrow, None - Circle, Circle - Arrow}"/>
		<property name="Stroke opacity" nameLabel="Standard_Shapes_property_stroke_opacity" type="string"/>
		<property name="Fill color" nameLabel="Standard_Shapes_property_fill_color" type="string"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
	</model>
<css>.standardshapes-wrapper {
    border: 0 solid black;
    padding: 0;
    margin: 0;
    overflow: hidden;
}
</css><view>&lt;div class="standardshapes-wrapper"&gt;

&lt;/div&gt;
</view><preview>&lt;div class="standardshapes-wrapper"&gt;

&lt;/div&gt;
</preview><presenter>function AddonStandard_Shapes_create(){
    var presenter = function() {};

    presenter.SHAPES = {
        LINE: 'Line',
        SQUARE: 'Square',
        RECTANGLE: 'Rectangle',
        CIRCLE: 'Circle',
        ELLIPSE: 'Ellipse'
    };

    presenter.LINE_ENDING = {
        NONE: 'None',
        CIRCLES: 'Circles',
        ARROWS: 'Arrows',
        NONE_AND_ARROW: 'None - Arrow',
        NONE_AND_CIRCLE: 'None - Circle',
        CIRCLE_AND_ARROW: 'Circle - Arrow'
    };

    presenter.ERROR_CODES = {
        'R01': "Rotation angle must be between 0 and 360 degrees!",
        'R02': "Rotation angle is not a number!",
        'ST1': "Stroke width must be a positive number",
        'ST2': "Stroke width is not a number!",
        'ST3': "Stroke color must be in RGB format (hexadecimal) and start with #",
        'ST4': "Stroke opacity must be a positive number between 0 and 1",
        'ST5': "Stroke opacity is not a number!",
        'F01': "Stroke color must be in RGB format (hexadecimal) and start with #",
        'RU1': "Addon dimensions are too small to draw line with endings (or stroke is too thick)!",
        'RU2': "Addon dimensions are too small to draw line with circle ending!",
        'RU3': "Addon dimensions are too small to draw line with circle-arrow ending!"
    };

    function rotatePoint(x, y, angle) {
        return {
            x: x * Math.cos(angle) - y * Math.sin(angle),
            y: x * Math.sin(angle) + y * Math.cos(angle)
        };
    }

    // Angle is counted in radians
    function rotatePoints(points, angle) {
        var rotatedPoints = [];

        for (var i = 0; i &lt; points.length; i++) {
            var x = points[i].x;
            var y = points[i].y;

            rotatedPoints.push(rotatePoint(x, y, angle));
        }

        return rotatedPoints;
    }

    function convertToRadians(degrees) {
        return degrees * Math.PI / 180;
    }

    function translatePoint(x, y, xOffset, yOffset) {
        return {
            x: x + xOffset,
            y: y + yOffset
        };
    }

    function translatePoints(points, xOffset, yOffset) {
        var translatedPoints = [];

        for (var i = 0; i &lt; points.length; i++) {
            var x = points[i].x;
            var y = points[i].y;

            translatedPoints.push(translatePoint(x, y, xOffset, yOffset));
        }

        return translatedPoints;
    }

    function calculateScale(points, width, height) {
        var farLeftX = 0;
        var farRightX = 0;
        var bottomY = 0;
        var topY = 0;

        for (var i = 0; i &lt; points.length; i++) {
            if (points[i].x &lt; farLeftX) {
                farLeftX = points[i].x;
            }
            if (points[i].y &lt; bottomY) {
                bottomY = points[i].y;
            }

            if (points[i].x &gt; farRightX) {
                farRightX = points[i].x;
            }
            if (points[i].y &gt; topY) {
                topY = points[i].y;
            }
        }

        var widthAfterRotation = Math.abs(farLeftX) + Math.abs(farRightX);
        var heightAfterRotation = Math.abs(bottomY) + Math.abs(topY);

        return Math.min(width / widthAfterRotation, height / heightAfterRotation);
    }

    function getScale(width, height, angle) {
        var points = [];
        points[0] = {x:0, y:0};
        points[1] = {x:width, y:0};
        points[2] = {x:width, y:height};
        points[3] = {x:0, y:height};

        var xOffset = -1 * width / 2;
        var yOffset = -1 * height / 2;

        var translatedPoints = translatePoints(points, xOffset, yOffset);
        var rotatedPoints = rotatePoints(translatedPoints, convertToRadians(angle));

        return calculateScale(rotatedPoints, width, height);
    }

    function drawShape(model, wrapper, width, height, angle) {
        var scale = getScale(width, height, angle);
        var length = Math.min(width, height);
        // Ugly fix for SVG elements that are smaller than 20px to position it's elements properly
        var paper = new Raphael(wrapper, width &lt; 20 ? 20 : width, height &lt; 20 ? 20 : height);

        switch (model.Shape) {
            case presenter.SHAPES.SQUARE:
                presenter.drawRectangle(paper, length, length, angle, scale);
                break;
            case presenter.SHAPES.RECTANGLE:
                presenter.drawRectangle(paper, width, height, angle, scale);
                break;
            case presenter.SHAPES.CIRCLE:
                presenter.drawEllipse(paper, length, length, angle, 1);
                break;
            case presenter.SHAPES.ELLIPSE:
                presenter.drawEllipse(paper, width, height, angle, scale);
                break;
            default:
                presenter.drawLine(paper, width, height, angle, scale);
        }
    }

    function presenterLogic(view, model) {
        presenter.$view = $(view);
        var containerDimensions = DOMOperationsUtils.getOuterDimensions(presenter.$view);
        var containerDistances = DOMOperationsUtils.calculateOuterDistances(containerDimensions);
        presenter.$view.css({
            width : (presenter.$view.width() - containerDistances.horizontal) + 'px',
            height : (presenter.$view.height() - containerDistances.vertical) + 'px'
        });
        presenter.configuration = presenter.validateModel(model);
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }
        var canvasWrapper = presenter.$view.find('.standardshapes-wrapper:first')[0];
        var wrapperDimensions = DOMOperationsUtils.getOuterDimensions(canvasWrapper);
        var wrapperDistances = DOMOperationsUtils.calculateOuterDistances(wrapperDimensions);

        var canvasWrapperWidth = presenter.$view.width() - wrapperDistances.horizontal;
        var canvasWrapperHeight = presenter.$view.height() - wrapperDistances.vertical;
        $(canvasWrapper).css({
            width : (canvasWrapperWidth &lt; 1 ? model.Width : canvasWrapperWidth) + 'px',
            height : (canvasWrapperHeight &lt; 1 ? model.Height : canvasWrapperHeight) + 'px'
        });

        var angle = parseInt(presenter.configuration.rotation);
        drawShape(model, canvasWrapper, canvasWrapperWidth, canvasWrapperHeight, angle);
    }

    presenter.applyStyles = function(element) {
        element.attr({
            'stroke-width': presenter.configuration.strokeWidth,
            'stroke': presenter.configuration.strokeColor,
            'stroke-opacity': presenter.configuration.strokeOpacity,
            'fill': presenter.configuration.fillColor,
            'fill-rule': 'evenodd'
        });

        if (presenter.configuration.cornersRoundings) {
            element.attr('stroke-linejoin', 'round');
        }
    };

    presenter.applyLineStyles = function(element) {
        presenter.applyStyles(element);
        element.attr('fill', presenter.configuration.strokeColor);
    };

    presenter.drawRectangle = function (paper, width, height, angle, scale) {
        var rectangle = paper.rect(presenter.configuration.strokeWidth / 2, presenter.configuration.strokeWidth / 2, width - presenter.configuration.strokeWidth, height - presenter.configuration.strokeWidth);
        presenter.applyStyles(rectangle);
        rectangle.transform("r" + angle + "s" + scale);
    };

    presenter.drawEllipse = function (paper, width, height, angle, scale) {
        var rectangle = paper.ellipse(width / 2, height / 2, (width - presenter.configuration.strokeWidth) / 2, (height - presenter.configuration.strokeWidth) / 2);
        presenter.applyStyles(rectangle);
        rectangle.transform("r" + angle + "s" + scale);
    };

    function transformShape(paper, element, scale, angle, width, height) {
        var roundedScale = Math.round(scale * 100) / 100; // Rounding scale to two decimal places
        var cx = parseInt(width / 2, 10);
        var cy = parseInt(height / 2, 10);

        element.transform("r" + angle + "," + cx + "," + cy);
        element.transform("...s" + roundedScale + "," + roundedScale + "," + cx + "," + cy);
    }

    function calculateLinePoints(width, height) {
        return [
            { x: 0, y: 0 },
            { x: width, y: 0 },
            { x: width, y: height },
            { x: 0, y: height}
        ];
    }

    function calculateArrowPoints(lineEnding, width, height, arrowHeight) {
        var points = [];

        switch(lineEnding) {
            case presenter.LINE_ENDING.ARROWS:
                points.push({
                    x: presenter.configuration.strokeWidth,
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: parseInt(arrowHeight, 10),
                    y: presenter.configuration.strokeWidth
                });
                points.push({
                    x: parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: presenter.configuration.strokeWidth
                });
                points.push({
                    x: width - presenter.configuration.strokeWidth,
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: height - presenter.configuration.strokeWidth
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: parseInt(arrowHeight, 10),
                    y: height - presenter.configuration.strokeWidth
                });
                break;
            case presenter.LINE_ENDING.NONE_AND_ARROW:
                points.push({
                    x: 0,
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: presenter.configuration.strokeWidth + 2
                });
                points.push({
                    x: width - presenter.configuration.strokeWidth,
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: height - presenter.configuration.strokeWidth - 2
                });
                points.push({
                    x: width - parseInt(arrowHeight, 10),
                    y: parseInt(height / 2, 10)
                });
                points.push({
                    x: 0,
                    y: parseInt(height / 2, 10)
                });
                break;
        }

        return points;
    }

    function calculateCirclePoints(lineEnding, width, height, horizontalAxis) {
        var points = {};

        switch(lineEnding) {
            case presenter.LINE_ENDING.CIRCLES:
                points.pointA = {
                    x: 2 * horizontalAxis + (presenter.configuration.strokeWidth / 2) + 1.5,
                    y: parseInt(height / 2, 10)
                };
                points.pointB = {
                    x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                    y: parseInt(height / 2, 10)
                };
                points.pointC = {
                    x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                    y: parseInt(height / 2, 10)
                };
                points.pointD = {
                    x: 2 * horizontalAxis + (presenter.configuration.strokeWidth / 2) + 1.5,
                    y: parseInt(height / 2, 10)
                };
                break;
            case presenter.LINE_ENDING.NONE_AND_CIRCLE:
                points.pointA = {
                    x: 0,
                    y: parseInt(height / 2, 10)
                };
                points.pointB = {
                    x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                    y: parseInt(height / 2, 10)
                };
                points.pointC = {
                    x: width - 2 * horizontalAxis - (presenter.configuration.strokeWidth / 2) - 1.5,
                    y: parseInt(height / 2, 10)
                };
                points.pointD = {
                    x: 0,
                    y: parseInt(height / 2, 10)
                };
        }

        return points;
    }

    function calculateCircleAndArrowPoints(width, height, arrowHeight) {
        var points = {};

        points.pointA = {
            x: parseInt(height + presenter.configuration.strokeWidth / 2, 10),
            y: parseInt(height / 2 + 1, 10)
        };
        points.pointB = {
            x: width - parseInt(arrowHeight, 10),
            y: parseInt(height / 2 + 1, 10)
        };
        points.pointC = {
            x: width - parseInt(arrowHeight, 10),
            y: presenter.configuration.strokeWidth
        };
        points.pointD = {
            x: width - presenter.configuration.strokeWidth,
            y: parseInt(height / 2, 10)
        };
        points.pointE = {
            x: width - parseInt(arrowHeight, 10),
            y: parseInt(height - presenter.configuration.strokeWidth, 10)
        };
        points.pointF = {
            x: width - parseInt(arrowHeight, 10),
            y: parseInt(height / 2 + 1, 10)
        };
        points.pointG = {
            x: parseInt(height + presenter.configuration.strokeWidth / 2, 10),
            y: parseInt(height / 2 + 1, 10)
        };

        return points;
    }

    function drawLineFromPoints(points) {
        var pathString = "M" + points[0].x + "," + points[0].y;
        for (var i = 1; i &lt; points.length; i++) {
            pathString += "L" + points[i].x + "," + points[i].y;
        }
        pathString += "L" + points[0].x + "," + points[0].y + "Z";

        return pathString;
    }

    presenter.drawLine = function (paper, width, height, angle, scale) {
        var base = height - 2 * presenter.configuration.strokeWidth;
        var arrowHeight = parseInt(base * Math.sqrt(3) / 2, 10);
        var spareSpace = width - 2 * arrowHeight - 2 * presenter.configuration.strokeWidth;

        if (presenter.configuration.lineEnding !== presenter.LINE_ENDING.NONE) {
            if (4 * presenter.configuration.strokeWidth + 2 &gt; height) {
                DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, 'RU1');
                return;
            }
        }

        if (arrowHeight * 2 + presenter.configuration.strokeWidth &lt;= height) {
            arrowHeight--;
        } else if (spareSpace &lt; 2 * presenter.configuration.strokeWidth) {
            arrowHeight = parseInt(width / 2 - 4 * presenter.configuration.strokeWidth, 10);
        }

        var radius = parseInt(height / 2, 10);
        var horizontalAxis = radius - (presenter.configuration.strokeWidth + 1) * 0.5;
        var verticalAxis = horizontalAxis + 0.5;

        var points = [];
        var pathString;

        switch(presenter.configuration.lineEnding) {
            case presenter.LINE_ENDING.NONE:
                points = calculateLinePoints(width, height);
                pathString = drawLineFromPoints(points);

                break;
            case presenter.LINE_ENDING.ARROWS:
                points = calculateArrowPoints(presenter.LINE_ENDING.ARROWS, width, height, arrowHeight);
                pathString = drawLineFromPoints(points);

                break;
            case presenter.LINE_ENDING.NONE_AND_ARROW:
                points = calculateArrowPoints(presenter.LINE_ENDING.NONE_AND_ARROW, width, height, arrowHeight);
                pathString = drawLineFromPoints(points);

                break;
            case presenter.LINE_ENDING.CIRCLES:
                if (4 * radius &gt; width - 1) {
                    DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, 'RU1');
                    return;
                }

                points = calculateCirclePoints(presenter.LINE_ENDING.CIRCLES, width, height, horizontalAxis);

                pathString = "M" + points.pointA.x + "," + points.pointA.y + "L" + points.pointB.x + "," + points.pointB.y;
                pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 0 " + points.pointC.x + " " + (points.pointC.y - 1);
                pathString += "L" + points.pointC.x + "," + points.pointC.y;
                pathString += "L" + points.pointD.x + "," + points.pointD.y;
                pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 1 " + points.pointA.x + " " + (points.pointA.y - 1);
                pathString += "L" + points.pointA.x + "," + points.pointA.y;
                pathString += "Z";

                break;
            case presenter.LINE_ENDING.NONE_AND_CIRCLE:
                if (2 * radius &gt; width - 1) {
                    DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, 'RU2');
                    return;
                }

                points = calculateCirclePoints(presenter.LINE_ENDING.NONE_AND_CIRCLE, width, height, horizontalAxis);

                pathString = "M" + points.pointA.x + "," + points.pointA.y + "L" + points.pointB.x + "," + points.pointB.y;
                pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 0 " + points.pointC.x + " " + (points.pointC.y - 1);
                pathString += "L" + points.pointC.x + "," + points.pointC.y;
                pathString += "L" + points.pointD.x + "," + points.pointD.y;
                pathString += "Z";
                break;
            case presenter.LINE_ENDING.CIRCLE_AND_ARROW:
                if (2 * radius + arrowHeight &gt; width - 1) {
                    DOMOperationsUtils.showErrorMessage(presenter.$view, presenter.ERROR_CODES, 'RU3');
                    return;
                }

                points = calculateCircleAndArrowPoints(width, height, arrowHeight);

                pathString = "M" + points.pointA.x + "," + points.pointA.y + "L" + points.pointB.x + "," + points.pointB.y;
                pathString += "L" + points.pointC.x + "," + points.pointC.y;
                pathString += "L" + points.pointD.x + "," + points.pointD.y;
                pathString += "L" + points.pointE.x + "," + points.pointE.y;
                pathString += "L" + points.pointF.x + "," + points.pointF.y;
                pathString += "L" + points.pointG.x + "," + points.pointG.y;
                pathString += "A" + horizontalAxis + " " + verticalAxis + " 90 1 1 " + points.pointA.x + " " + (points.pointA.y - 1);
                pathString += "L" + points.pointA.x + "," + points.pointA.y;
                pathString += "Z";
                break;
        }

        var path = paper.path(pathString);
        presenter.applyLineStyles(path);
        transformShape(paper, path, scale, angle, width, height);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model);
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.validateModel = function(model) {
        var shape = model.Shape ? model.Shape : presenter.SHAPES.LINE;

        var rotation = model["Rotation angle"];
        if (!rotation) {
            rotation = 0;
        } else {
            rotation = parseFloat(rotation);
            if (isNaN(rotation)) {
                return { isError: true, errorCode: 'R02' };
            }

            if (rotation &lt; 0 || rotation &gt; 360) {
                return { isError: true, errorCode: 'R01' };
            }
        }

        var strokeWidth = model["Stroke width"];
        if (!strokeWidth) {
            strokeWidth = 1;
        } else {
            strokeWidth = parseFloat(strokeWidth);
            if (isNaN(strokeWidth)) {
                return { isError: true, errorCode: 'ST2' };
            }

            if (strokeWidth &lt;= 0) {
                return { isError: true, errorCode: 'ST1' };
            }
        }

        var strokeColor = model["Stroke color"];
        var regExp = new RegExp("#[0-9a-fA-F]+");
        var colorMatch;

        if (!strokeColor) {
            strokeColor = "#000";
        } else {
            if (strokeColor.length &lt; 4 || strokeColor.length &gt; 7) {
                return { isError: true, errorCode: 'ST3' };
            }

            colorMatch = strokeColor.match(regExp);
            if (!colorMatch || colorMatch === null || colorMatch.length &lt; 1) {
                return { isError: true, errorCode: 'ST3' };
            }
            if (colorMatch[0].length &lt; strokeColor.length) {
                return { isError: true, errorCode: 'ST3' };
            }
        }

        var fillColor = model["Fill color"];
        if (!fillColor) {
            fillColor = "#FFF";
        } else {
            if (fillColor.length &lt; 4 || fillColor.length &gt; 7) {
                return { isError: true, errorCode: 'F01' };
            }

            colorMatch = fillColor.match(regExp);
            if (!colorMatch || colorMatch === null || colorMatch.length &lt; 1) {
                return { isError: true, errorCode: 'F01' };
            }
            if (colorMatch[0].length &lt; fillColor.length) {
                return { isError: true, errorCode: 'F01' };
            }
        }

        var cornersRoundings = model["Corners rounding"];
        if (!cornersRoundings) {
            cornersRoundings = false;
        } else {
            cornersRoundings = cornersRoundings === 'True';
        }

        var strokeOpacity = model["Stroke opacity"];
        if (!strokeOpacity) {
            strokeOpacity = 1;
        } else {
            strokeOpacity = parseFloat(strokeOpacity);
            if (isNaN(strokeOpacity)) {
                return { isError: true, errorCode: 'ST5' };
            }

            if (strokeOpacity &lt; 0 || strokeOpacity &gt; 1) {
                return { isError: true, errorCode: 'ST4' };
            }
        }

        var lineEnding = model["Line ending"];
        if (!lineEnding) {
            lineEnding = presenter.LINE_ENDING.NONE;
        }

        var isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        return {
            isError: false,
            shape: shape,
            rotation: rotation,
            strokeWidth: strokeWidth,
            strokeColor: strokeColor,
            fillColor: fillColor,
            cornersRoundings: cornersRoundings,
            strokeOpacity: strokeOpacity,
            lineEnding: lineEnding,
            isVisibleByDefault: isVisibleByDefault,
            isVisible: isVisibleByDefault
        };
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.reset = function () {
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function (state) {
        var isVisible = JSON.parse(state).isVisible;

        presenter.configuration.isVisible = isVisible;
        presenter.setVisibility(isVisible);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var $printableView = $("&lt;div&gt;&lt;/div&gt;");
        $printableView.addClass("printable_addon_Standard_Shapes");
        $printableView.css("height", model.Height + "px");
        $printableView.css("width", model.Width + "px");
        var $wrapper = $("&lt;div class=\"standardshapes-wrapper\"&gt;&lt;/div&gt;");
        $printableView.append($wrapper);
        $printableView.css("visibility", "hidden");
        $('body').append($printableView);
        presenterLogic($printableView[0], model);
        $printableView.detach();
        $printableView.css("visibility", "");
        return $printableView[0].outerHTML;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Submit" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property isLocalized="true" name="Text" nameLabel="Submit_property_text" type="string"/>
        <property displayName="Text Selected" isLocalized="true" name="TextSelected" nameLabel="Submit_property_text_selected" type="string"/>
	</model>
<css>.submit-wrapper,
.submit-wrapper .submit-container,
.submit-wrapper .submit-container .submit-button {
    width: 100%;
    height: 100%;
}

.submit-wrapper .submit-container .submit-button {
    background: url('resources/submit-button.png') no-repeat center;
    cursor: pointer;
    text-align: center;
}
</css><view>&lt;div class="submit-wrapper"&gt;
    &lt;div class="submit-container"&gt;
        &lt;div class="submit-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="submit-wrapper"&gt;
    &lt;div class="submit-container"&gt;
        &lt;div class="submit-button"&gt;

        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonSubmit_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);

        presenter.runEndedDeferred = new $.Deferred();
        presenter.runEnded = presenter.runEndedDeferred.promise();
    };

    presenter.onEventReceived = function(eventName) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        }
    };

    presenter.createEventData = function (additionalEventData) {
        var eventData = { 'source': presenter.configuration.addonID };
        for (var key in additionalEventData) {
            eventData[key] = additionalEventData[key];
        }
        return eventData;
    };

    function getEventObject(item, value, score) {
        return {
            'source': presenter.configuration.addonID,
            'item': item != undefined ? item : "",
            'value': value != undefined ? value : "",
            'score': score != undefined ? score : ""
        };
    }

    presenter.sendEvent = function(eventName, eventData) {
        var eventObject = getEventObject(eventData.item, eventData.value, eventData.score);

        presenter.eventBus.sendEvent(eventName, eventObject);
    };

    presenter.ERROR_CODES = {

    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.validateModel = function(model, isPreview) {
        var buttonText = model['Text'],
            buttonTextSelected = model['TextSelected'],
            isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);

        return {
            'buttonText' : buttonText,
            'buttonTextSelected' : buttonTextSelected,
            'addonID' : model['ID'],
            'isVisible' : isVisible,
            'isVisibleByDefault': isVisible
        }
    };

    function getAllOfTheModulesThatImplementIsAttempted() {
        var pageIndex = presenter.playerController.getCurrentPageIndex(),
            ids = presenter.playerController.getPresentation().getPage(pageIndex).getModulesAsJS(),
            modules = [];

        for(var i = 0; i &lt; ids.length; i++){
            var currentModule = presenter.playerController.getModule(ids[i]);

            if (currentModule &amp;&amp; currentModule.isAttempted !== undefined) {
                modules.push(currentModule);
            }
        }
        return modules;
    }

    function areAllModulesAttempted() {
        var areAllAttempted = true;

        $.each(presenter.modulesOnPage, function() {
            if (!this.isAttempted()) {
                areAllAttempted = false;
                return false; // break;
            }
        });

        return areAllAttempted;
    }

    function runLogic(view, model, isPreview) {
        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();

        presenter.$view = $(view);

        presenter.configuration = presenter.validateModel(model, isPreview);

        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.submitButton = presenter.$view.find('.submit-button');
        presenter.submitButton.html(presenter.configuration.buttonText);
        presenter.submitWrapper = presenter.$view.find('.submit-wrapper');

        presenter.pageLoaded.then(function() {

            presenter.modulesOnPage = getAllOfTheModulesThatImplementIsAttempted();

            presenter.submitButton.click(function(e) {
                e.stopPropagation();

                var isSelected = $(presenter.submitWrapper).hasClass('selected');

                if (isSelected) {
                    presenter.submitWrapper.removeClass('selected');
                    presenter.submitButton.html(presenter.configuration.buttonText);

                    presenter.playerController.getCommands().uncheckAnswers();
                    presenter.sendEvent('State', presenter.createEventData({ 'value' : 0 }));

                } else if (areAllModulesAttempted()) {
                    presenter.submitWrapper.addClass('selected');
                    presenter.submitButton.html(presenter.configuration.buttonTextSelected);

                    presenter.playerController.getCommands().checkAnswers();
                    presenter.sendEvent('Submitted', presenter.createEventData());
                    presenter.sendEvent('State', presenter.createEventData({ 'value' : 1 }));
                } else {
                    presenter.sendEvent('NotAllAttempted', presenter.createEventData());
                }

            });

            presenter.runEndedDeferred.resolve();
        });
    }

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    presenter.setShowErrorsMode = function(){
    };

    presenter.setWorkMode = function(){
    };

    presenter.reset = function(){
        presenter.submitWrapper.removeClass('selected');
        presenter.submitButton.html(presenter.configuration.buttonText);
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
    };

    presenter.getErrorCount = function(){
        return 0;
    };

    presenter.getMaxScore = function(){
        return 0;
    };

    presenter.getScore = function(){
        return 0;
    };

    presenter.getState = function(){
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setState = function(rawState){
        var state = JSON.parse(rawState);

        presenter.setVisibility(state.isVisible);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.configuration.isVisible = isVisible;
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Sudoku" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Rows Values" isLocalized="true" name="Values" nameLabel="Sudoku_property_values" type="text"/>
        <property name="isDisable" nameLabel="Sudoku_property_is_disabled" type="boolean"/>
        <property name="isActivity" nameLabel="Sudoku_property_is_activity" type="boolean"/>
    </model>
<css>.board input.cell {
            font-size: 15pt;
            text-align: center;
        }
.board input.wrong {
            background-color: #ff2a2a; 
        }
.board input.correct {
            background-color: #99FF55; 
        }  
.board input.filled {
            background-color: white; 
        } 

        .c {
            border: 1px black solid;
            -webkit-border-radius: 0px;
            -khtml-border-radius: 0px;
            -moz-border-radius: 0px;
            width: 2.2em !important;
            height: 2.2em !important;
            padding: 0px;
        }

        .v {
            border-right: 0;
        }

        .h {
            border-bottom: 0;
        }
        .filled{
            color: blue;
            }
        .sudoku-counter{
            color:red;
        background-color: #f2f2f2;        
            }
</css><view>&lt;p class="sudoku-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id = "sudoku-wrapper" class= "sudoku-wrapper"&gt;
&lt;/div&gt;
</view><preview>&lt;p class="sudoku-counter" style="position: absolute;"&gt;&lt;/p&gt;
&lt;div id = "sudoku-wrapper" class= "sudoku-wrapper"&gt;
&lt;/div&gt;
</preview><presenter>function AddonSudoku_create(){

    var presenter = function(){};

    presenter.isActivity = false;
    presenter.isErrorCheckingMode = false;
    presenter.isActivity = false;
    presenter.initialView = [];
    presenter.initialViewNumbers = 0;
    presenter.currentViewNumbers = 0;
    presenter.isErrorCheckingMode = false;
    presenter.cells = [];
    presenter.currentAnswer = [];
    presenter.eventBus = '';
    presenter.isShowAnswerMode = false;

    function displayText() {
        var textToDisplay = presenter.model['Text to be displayed'],
            isTextColored = presenter.model['Color text'] === 'True',
            $textContainer = presenter.$view.find('.some-text-container');

        $textContainer.text(textToDisplay);
        if (isTextColored) {
            $textContainer.css('color', 'red');
        }
    }



    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'enable'.toLowerCase():
                presenter.enable();
                break;
            case 'disable'.toLowerCase():
                presenter.disable();
                break;
            case 'show'.toLowerCase():
                presenter.show();
                break;
            case 'hide'.toLowerCase():
                presenter.hide();
                break;
            case 'isAttempted'.toLowerCase():
                presenter.isAttempted();
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
        }
    };

    presenter.drawSudoku = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        presenter.modelID = model.ID;
        presenter.Height = parseInt(model.Height,10);
        presenter.Width = parseInt(model.Width,10);
        presenter.number = Math.floor((Math.random()*100)+1);


        var fig = '&lt;table&gt;';
        fig += '&lt;tr&gt;';

        fig += '&lt;td valign="top"&gt;';
        fig += '&lt;form class="board" autocomplete="off"&gt;';
        fig += '&lt;table cellspacing="0" cellpadding="0" border="0"&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c11'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c12'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c13'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c14'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c15'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c16'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c17'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c18'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c19'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c21'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c22'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c23'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c24'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c25'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c26'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c27'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c28'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c29'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c31'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c32'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c33'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c34'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c35'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c36'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c37'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c38'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c39'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c41'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c42'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c43'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c44'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c45'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c46'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c47'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c48'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c49'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c51'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c52'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c53'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c54'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c55'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c56'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c57'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c58'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c59'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c61'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c62'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c63'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c64'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c65'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c66'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c67'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c68'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c69'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c71'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c72'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c73'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c74'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c75'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c76'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c77'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c78'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c79'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c81'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c82'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c83'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c84'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c85'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c86'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c87'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v h cell active" id="c88'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c   h cell active" id="c89'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;tr&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c91'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c92'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c93'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c94'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c95'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c96'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c97'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c v  cell active" id="c98'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;td&gt;&lt;input class="c    cell active" id="c99'+ presenter.modelID + presenter.number +'" maxlength="1" autocomplete="off" /&gt;&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;/table&gt;';
        fig += '&lt;/form&gt;';
        fig += '&lt;/td&gt;';
        fig += '&lt;/tr&gt;';
        fig += '&lt;/table&gt;';

        return fig;
    };

    presenter.checkRowsValues = function(value, view){
        var regExp = new RegExp('\n');
        $counter = $(view).find('.sudoku-counter');
        var step = 1;
        var enters = 0;
        for(i = 0; i&lt; value.length; i++){
            if(value[i] != ' ') {
                if((parseInt(value[i],10) &lt; 1 || isNaN(value[i],10)) &amp;&amp; !value[i].match(regExp) &amp;&amp; value[i] != '_'){
                    $counter.text('Row '+(enters + 1)+' has incorrect value.');
                    return false;
                }
                if(step &gt; 10){
                    $counter.text('Row '+(enters + 1)+' has more than 9 values.');
                    return false;
                }
                if(enters &gt; 8){
                    $counter.text('There are more than 9 rows.');
                    return false;
                }
                if(value[i].match(regExp)) {
                    enters++;
                    step = 1;
                } else{
                    step++;
                }
            }

        }
        return true;
    };


    presenter.validate = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        $counter = $(view).find('.sudoku-counter');

        if(!presenter.checkRowsValues(model.Values, view)){
            return false;
        }

        return true;
    };

    presenter.init = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        presenter.modelID = model.ID;
        presenter.isActivity = model.isActivity == "True" ? true : false;
        presenter.isDisable = model.isDisable == "True" ? true : false;
        presenter.wasDisable = model.isDisable == "True" ? true : false;
        presenter.wasVisible = model["Is Visible"] == 'True';
        presenter.isVisible = model["Is Visible"] == 'True';
        presenter.Values = model.Values;


        var myDiv =  $(view).find('.sudoku-wrapper')[0];

        var figureSudoku = presenter.drawSudoku(view, model);
        $(myDiv).append(figureSudoku);

        if(presenter.isDisable){
            presenter.disable();
        }

        presenter.cells = [[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9],[1,2,3,4,5,6,7,8,9]];
        presenter.currentAnswer = [['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','','']];
        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                presenter.cells[j-1][i-1] = presenter.$view.find('"#c'+j+i+ presenter.modelID + presenter.number + '"');
            }
        }

        presenter.drawInitial(model.Values);
    };

    presenter.run = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;

        presenter.Values = model.Values;
        var score = '';
        var test = false;

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);

        if(presenter.validate(view,model)){
            presenter.init(view, model);
            presenter.setVisibility(presenter.isVisible);

            if(model.isActivity == "True"){
                presenter.correctAnswer = presenter.checkSudoku(view);
            }

            presenter.$view.find("input.active").change(function() {

                if(parseInt(this.value,10) &lt; 1 || isNaN(this.value,10) || this.value == ' '){
                    test = true;
                    presenter.cells[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)].attr("value", '');

                    if(presenter.currentAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] != ''){
                        presenter.currentViewNumbers--;
                        presenter.currentAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] = '';
                    }

                } else{

                    test = false;

                }

                if(!test) {

                    var id = this.id.slice(2,3) + "-" + this.id.slice(1,2);

                    if(presenter.isActivity){
                        score = this.value == presenter.correctAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] ? 1 : 0;
                    } else {
                        score = '';

                    }
                    presenter.triggerFrameChangeEvent(this.value, id, score);

                    if(presenter.isActivity){
                        if(this.value != ''){
                            if(presenter.currentAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] == ''){
                                presenter.currentAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] = this.value;
                                presenter.currentViewNumbers++;
                            } else{
                                presenter.currentAnswer[(this.id.slice(1,2)-1)][(this.id.slice(2,3)-1)] = this.value;
                            }
                        } else{
                            presenter.currentViewNumbers--;
                        }
                    }
                }

                if(presenter.currentViewNumbers == 81){
                    presenter.isAllOkEvent();
                }
            });



        }
    };

    presenter.createPreview = function(view, model) {
        presenter.$view = $(view);
        presenter.model = model;

        if(presenter.validate(view,model)){
            presenter.init(view, model);

            presenter.setVisibility(true);

            if(model.isActivity == "True"){
                presenter.checkSudoku(view);
            }
        }

    };
    presenter.isAllOkEvent = function(){
        if(presenter.isAllOk()){
            presenter.triggerFrameChangeEvent("allOk", "", "");
        }
    };

    presenter.isAllOkChecker = function(){
        var correct = 0;
        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                if(presenter.currentAnswer[j-1][i-1] == presenter.correctAnswer[j-1][i-1])
                {
                    correct++;
                } else{
                    return correct;
                }
            }
        }
        return correct;
    };

    presenter.isAllOk = function(){
        var check = presenter.isAllOkChecker();
        if(check == 81){
            return true;
        } else{
            return false;
        }
    };

    presenter.isAttempted = function(){
        presenter.hideAnswers();
        if(presenter.isActivity){
            return presenter.initialViewNumbers == presenter.currentViewNumbers ? false: true;
        } else{
            return true;
        }

    };

    presenter.checkSudoku = function(view) {
        function dlx_cover(c)
        {
            c.right.left = c.left;
            c.left.right = c.right;
            for (var i = c.down; i != c; i = i.down) {
                for (var j = i.right; j != i; j = j.right) {
                    j.down.up = j.up;
                    j.up.down = j.down;
                    j.column.size--;
                }
            }
        }

        function dlx_uncover(c)
        {
            for (var i = c.up; i != c; i = i.up) {
                for (var j = i.left; j != i; j = j.left) {
                    j.column.size++;
                    j.down.up = j;
                    j.up.down = j;
                }
            }
            c.right.left = c;
            c.left.right = c;
        }

        function dlx_search(head, solution, k, solutions, maxsolutions)
        {
            if (head.right == head) {
                solutions.push(solution.slice(0));
                if (solutions.length &gt;= maxsolutions) {
                    return solutions;
                }
                return null;
            }
            var c = null;
            var s = 99999;
            for (var j = head.right; j != head; j = j.right) {
                if (j.size == 0) {
                    return null;
                }
                if (j.size &lt; s) {
                    s = j.size;
                    c = j;
                }
            }

            dlx_cover(c);
            for (var r = c.down; r != c; r = r.down) {
                solution[k] = r.row;
                for (var j = r.right; j != r; j = j.right) {
                    dlx_cover(j.column);
                }
                var s = dlx_search(head, solution, k+1, solutions, maxsolutions);
                if (s != null) {
                    return s;
                }
                for (var j = r.left; j != r; j = j.left) {
                    dlx_uncover(j.column);
                }
            }
            dlx_uncover(c);
            return null;
        }

        function dlx_solve(matrix, skip, maxsolutions)
        {
            var columns = new Array(matrix[0].length);
            for (var i = 0; i &lt; columns.length; i++) {
                columns[i] = new Object;
            }
            for (var i = 0; i &lt; columns.length; i++) {
                columns[i].index = i;
                columns[i].up = columns[i];
                columns[i].down = columns[i];
                if (i &gt;= skip) {
                    if (i-1 &gt;= skip) {
                        columns[i].left = columns[i-1];
                    }
                    if (i+1 &lt; columns.length) {
                        columns[i].right = columns[i+1];
                    }
                } else {
                    columns[i].left = columns[i];
                    columns[i].right = columns[i];
                }
                columns[i].size = 0;
            }
            for (var i = 0; i &lt; matrix.length; i++) {
                var last = null;
                for (var j = 0; j &lt; matrix[i].length; j++) {
                    if (matrix[i][j]) {
                        var node = new Object;
                        node.row = i;
                        node.column = columns[j];
                        node.up = columns[j].up;
                        node.down = columns[j];
                        if (last) {
                            node.left = last;
                            node.right = last.right;
                            last.right.left = node;
                            last.right = node;
                        } else {
                            node.left = node;
                            node.right = node;
                        }
                        columns[j].up.down = node;
                        columns[j].up = node;
                        columns[j].size++;
                        last = node;
                    }
                }
            }
            var head = new Object;
            head.right = columns[skip];
            head.left = columns[columns.length-1];
            columns[skip].left = head;
            columns[columns.length-1].right = head;
            solutions = [];
            dlx_search(head, [], 0, solutions, maxsolutions);
            return solutions;
        }

        function solve_sudoku(grid)
        {
            var mat = [];
            var rinfo = [];
            for (var i = 0; i &lt; 9; i++) {
                for (var j = 0; j &lt; 9; j++) {
                    var g = grid[i][j] - 1;
                    if (g &gt;= 0) {
                        var row = new Array(324);
                        row[i*9+j] = 1;
                        row[9*9+i*9+g] = 1;
                        row[9*9*2+j*9+g] = 1;
                        row[9*9*3+(Math.floor(i/3)*3+Math.floor(j/3))*9+g] = 1;
                        mat.push(row);
                        rinfo.push({'row': i, 'col': j, 'n': g+1});
                    } else {
                        for (var n = 0; n &lt; 9; n++) {
                            var row = new Array(324);
                            row[i*9+j] = 1;
                            row[9*9+i*9+n] = 1;
                            row[9*9*2+j*9+n] = 1;
                            row[9*9*3+(Math.floor(i/3)*3+Math.floor(j/3))*9+n] = 1;
                            mat.push(row);
                            rinfo.push({'row': i, 'col': j, 'n': n+1});
                        }
                    }
                }
            }
            var solutions = dlx_solve(mat, 0, 2);
            if (solutions.length &gt; 0) {
                var r = solutions[0];
                for (var i = 0; i &lt; r.length; i++) {
                    grid[rinfo[r[i]]['row']][rinfo[r[i]]['col']] = rinfo[r[i]]['n'];
                }
                return solutions.length;
            }
            return 0;
        }


        $counter = $(view).find('.sudoku-counter');
        $counter.text('');
        var g = [];
        for (var i = 1; i &lt;= 9; i++) {
            var r = [];
            for (var j = 1; j &lt;= 9; j++) {
                r.push(presenter.cells[i-1][j-1].attr("value"));
            }
            g.push(r);

        }
        var r = solve_sudoku(g);
        if (r &gt; 0) {
            if (r &gt; 1) {
                $counter = $(view).find('.sudoku-counter');
                $counter.text('There is more than one solution.');
                presenter.isDisable = true;
                return false;
            }
        } else {
            $counter = $(view).find('.sudoku-counter');
            $counter.text('There is no solution.');
            presenter.isDisable = true;
            return false;
        }

        presenter.isActivity = true;
        return g;
    };

    presenter.clearSudoku = function() {

        for (var i = 1; i &lt;= 9; i++) {
            for (var j = 1; j &lt;= 9; j++) {
                presenter.cells[i-1][j-1].attr("value", '');
            }

        }

        presenter.currentAnswer = [['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','',''],['','','','','','','','','']];
        presenter.drawInitial(presenter.Values);
    };

    presenter.drawInitial = function (value){
        presenter.initialViewNumbers = 0;
        presenter.currentViewNumbers = 0;

        var regExp = new RegExp('\n');
        var j= 1;
        var step = 1;
        for(i = 0; i&lt; value.length; i++){
            if(value[i] != ' ') {
                if(value[i].match(regExp)) {
                    j++;
                    step = 1;
                } else{
                    $element = presenter.cells[j-1][step-1];
                    if(value[i] == '_'){
                        $($element).attr("value", '');
                    } else{
                        $($element).attr("value", value[i]);
                        $($element).attr( "readonly", "readonly" );
                        $($element).addClass("filled");
                        $($element).removeClass("active");
                        presenter.initialViewNumbers++;
                        presenter.currentViewNumbers++;
                        presenter.currentAnswer[j-1][step-1] = value[i];
                    }
                    step++;
                }
            }

        }
    };

    presenter.disable = function(){
        presenter.hideAnswers();
        presenter.isDisable = true;
        presenter.$view.find('input.active').attr( "readonly", "readonly" );
        presenter.$view.find('input.cell').addClass('disable');

    };

    presenter.enable = function(){
        presenter.hideAnswers();
        presenter.isDisable = false;
        presenter.$view.find('input.active').removeAttr("readonly");
        presenter.$view.find('input.cell').removeClass('disable');
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.hideAnswers();
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function() {
        presenter.hideAnswers();
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.checkMyCurrentAnswer = function(){

        var test = 0;
        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                $element = presenter.cells[j-1][i-1];
                if($($element).attr("value") != ''){
                    if(presenter.correctAnswer[j-1][i-1] != $($element).attr("value")){
                        $($element).addClass("wrong");
                        $($element).removeClass("correct");

                    } else{
                        $($element).removeClass("wrong");
                        $($element).addClass("correct");
                    }
                } else{
                    $($element).removeClass("wrong");
                    $($element).removeClass("correct");
                }
            }
        }


    };


    presenter.getState = function () {
        presenter.isErrorCheckingMode = false;
        var isVisible = presenter.isVisible;
        var wasVisible = presenter.wasVisible;
        var initialViewNumbers = presenter.initialViewNumbers;
        var wasDisable = presenter.wasDisable;
        var isDisable = presenter.isDisable;
        var Values = presenter.Values;
        var currentViewNumbers = presenter.currentViewNumbers;

        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                $element = presenter.cells[j-1][i-1];
                $($element).removeClass("wrong");
                $($element).removeClass("correct");
            }
        }

        var current = presenter.currentAnswer;

        return JSON.stringify({
            initialViewNumbers: initialViewNumbers,
            isVisible: isVisible,
            wasVisible: wasVisible,
            wasDisable: wasDisable,
            isDisable: isDisable,
            Values: Values,
            current: current,
            currentViewNumbers: currentViewNumbers
        });

    };

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state);
        presenter.initialViewNumbers = parsedState.initialViewNumbers;
        presenter.isVisible = parsedState.isVisible;
        presenter.wasVisible = parsedState.wasVisible;
        presenter.Values = parsedState.Values;
        presenter.wasDisable = parsedState.wasDisable;
        presenter.isDisable = parsedState.isDisable;
        presenter.setVisibility(presenter.isVisible);
        presenter.currentAnswer = parsedState.current;
        presenter.currentViewNumbers = parsedState.currentViewNumbers;

        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                $(presenter.cells[j-1][i-1]).attr("value", presenter.currentAnswer[j-1][i-1]);
            }
        }

        if(presenter.isDisable){
            presenter.$view.find("input.active").attr( "readonly", "readonly" );
            presenter.$view.find("input.cell").addClass('disable');
        } else {
            presenter.$view.find("input.active").removeAttr("readonly");
            presenter.$view.find("input.cell").removeClass('disable');
        }
    };

    presenter.reset = function () {
        presenter.hideAnswers();
        presenter.setWorkMode();
        presenter.isErrorCheckingMode = false;
        presenter.isShowAnswerMode = false;
        presenter.clearSudoku();

        presenter.drawInitial(presenter.Values);

        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                $element = presenter.cells[j-1][i-1];
                $($element).removeClass("wrong");
                $($element).removeClass("correct");
            }
        }
        presenter.isVisible = presenter.wasVisible;
        presenter.setVisibility(presenter.wasVisible);
        presenter.isDisable = presenter.wasDisable;
        presenter.isDisable === true ?  presenter.disable() : presenter.enable();
    };

    presenter.getMaxScore = function () {

        if(presenter.isActivity === true) {
            var maxScore = 81 - presenter.initialViewNumbers;
            return maxScore;
        } else {
            return 0;
        }
    };


    presenter.getScore = function () {

        presenter.hideAnswers();

        if(presenter.isActivity === true) {
            var correct = 0;
            for(j=1;j&lt;=9;j++){
                for(i=1;i&lt;=9;i++){
                    $element = presenter.cells[j-1][i-1];
                    if($($element).attr("value") != ''){
                        if(presenter.correctAnswer[j-1][i-1] == $($element).attr("value")){
                            correct++;
                        }
                    }
                }
            }
            var score = correct - presenter.initialViewNumbers;
            return score;
        } else {
            return 0;
        }
    };

    presenter.getErrorCount = function () {

        presenter.hideAnswers();

        if(presenter.isActivity === true) {
            var errors = 0;
            for(j=1;j&lt;=9;j++){
                for(i=1;i&lt;=9;i++){
                    $element = presenter.cells[j-1][i-1];
                    if($($element).attr("value") != ''){
                        if(presenter.correctAnswer[j-1][i-1] != $($element).attr("value")){
                            errors++;
                        }
                    }
                }
            }
            return errors;
        } else {
            return 0;
        }
    };



    presenter.neutralOption = function(){
        return presenter.getCurrentTime() == presenter.InitialTime ? 1 : 0;
    };

    presenter.setShowErrorsMode = function () {

        presenter.isErrorCheckingMode = true;


        presenter.hideAnswers();

        if(presenter.isActivity === true) {

            presenter.$view.find("input.active").attr( "readonly", "readonly" );
            presenter.$view.find("input.active").addClass("check");
            presenter.checkMyCurrentAnswer();


        }
    };

    presenter.setWorkMode = function () {

        presenter.isErrorCheckingMode = false;
        presenter.$view.find("input.active").removeClass("check");

        for(j=1;j&lt;=9;j++){
            for(i=1;i&lt;=9;i++){
                $element = presenter.cells[j-1][i-1];
                $($element).removeClass("wrong");
                $($element).removeClass("correct");
            }
        }
        if(!presenter.isDisable){
            presenter.$view.find("input.active").removeAttr("readonly");
        }

    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

    presenter.createEventData = function(value, id, score) {
        return {
            source : presenter.modelID,
            item : "" + id,
            value : '' + value,
            score : '' + score
        };
    };

    presenter.triggerFrameChangeEvent = function(value, id, score) {
        var eventData = presenter.createEventData(value, id, score);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.showAnswers = function () {

        presenter.setWorkMode();

        if(presenter.isActivity === true) {
            presenter.isShowAnswerMode = true;
            presenter.isErrorCheckingMode = true;

            presenter.$view.find("input.active").attr( "readonly", "readonly" );
            presenter.$view.find("input.active").addClass("showAnswers");

            for(j=1;j&lt;=9;j++){
                for(i=1;i&lt;=9;i++){
                    $(presenter.cells[j-1][i-1]).attr("value", presenter.correctAnswer[j-1][i-1]);
                }
            }
        }
    };

    presenter.hideAnswers = function () {

        if(presenter.isActivity === true &amp;&amp; presenter.isShowAnswerMode == true) {
            presenter.isErrorCheckingMode = false;
            presenter.$view.find("input.active").removeClass("showAnswers");
            presenter.$view.find("input.active").removeAttr("readonly");

            for(j=1;j&lt;=9;j++){
                for(i=1;i&lt;=9;i++){
                    $(presenter.cells[j-1][i-1]).attr("value", presenter.currentAnswer[j-1][i-1]);
                }
            }
        }


    };

    presenter.onEventReceived = function (eventName) {

        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="SwiffyAnimation" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Animations" nameLabel="SwiffyAnimation_property_animations" type="list">
			<property displayName="Swiffy File" name="swiffyobject" nameLabel="SwiffyAnimation_property_swiffy_object" type="file"/>
			<property displayName="Auto Play" name="autoPlay" nameLabel="SwiffyAnimation_property_auto_play" type="boolean"/>
			<property displayName="Disable Transparent Background" name="disableTransparentBackground" nameLabel="SwiffyAnimation_property_disable_transparent_background" type="boolean"/>
            <property displayName="Do Not Preload" name="doNotPreload" nameLabel="SwiffyAnimation_property_do_not_preload" type="boolean"/>
		</property>
		<property displayName="Initial Animation" name="initialAnimation" nameLabel="SwiffyAnimation_property_initial_animation" type="string"/>
	</model>
<css>.swiffycontainer {
	width: 100%;
	height: 100%;
}

.swiffyAnimation {
	position: absolute;
}

.swiffy-loading-image {
	position: absolute;
	width: 30px;
	height: 30px;
	border: 0;
	outline: 0;
}
</css><view>&lt;div class="swiffyContainer"&gt;&lt;/div&gt;
&lt;img src="" alt="Loading..." class="swiffy-loading-image" /&gt;
</view><preview>&lt;div class="swiffyContainer"&gt;&lt;/div&gt;
&lt;img src="" alt="Loading..." class="swiffy-loading-image" style="display: none;" /&gt;
</preview><presenter>function AddonSwiffyAnimation_create(){

    var presenter = function(){};

    presenter.run = function(view, model){
        presenter.view = view;
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model['ID'];
        presenter.Animations = model.Animations;
        presenter.swiffyContainer = $(view).find('.swiffyContainer')[0];
        presenter.stage = [];
        presenter.swiffyObject = [];
        presenter.swiffyItem = [];
        presenter.animsLoaded = [];
        presenter.animsRunning = [];
        presenter.originalDisplay = presenter.$view.css('display') ? presenter.$view.css('display') : 'block';

        presenter.isVisible = model["Is Visible"] == 'True';
        presenter.setVisibility(presenter.isVisible);

        presenter.animationLoadedDeferred = [];
        presenter.animationLoaded = [];

        //show loading icon
        var loadingSrc = DOMOperationsUtils.getResourceFullPath(presenter.playerController, "media/loading.gif");
        presenter.loadingIconImg = $(view).find('.swiffy-loading-image')[0];
        $(presenter.loadingIconImg).css({
            left: (presenter.model.Width/2)-15 + 'px',
            top: (presenter.model.Height/2)-15 + 'px'
        });
        $(presenter.loadingIconImg).attr('src',loadingSrc);

        //determine presenter.currentAnimationItem
        if(model.initialAnimation === ""){
            model.initialAnimation = 1;
        } else {
            //incorrect model.initialAnimation number -&gt; error
            if( model.initialAnimation &lt; 1 || model.initialAnimation &gt; presenter.Animations.length || model.initialAnimation%1 !== 0 ){
                presenter.errorHandler("Error: Incorrect Initial Animation number.");
                $(presenter.loadingIconImg).css('display','none');
                return;
            }
            model.initialAnimation = parseInt(model.initialAnimation, 10);
        }
        presenter.currentAnimationItem = model.initialAnimation;

        //loop through Animations
        $(presenter.Animations).each(function(i, animation){
            //initiate deferred for every animation
            presenter.animationLoadedDeferred[i+1] = new $.Deferred();
            presenter.animationLoaded[i+1] = presenter.animationLoadedDeferred[i+1].promise();

            //empty Swiffy Object file -&gt; error
            if(animation.swiffyobject === ''){
                presenter.errorHandler("Error: Swiffy Object in Item "+(i+1)+" is missing.");
                $(presenter.loadingIconImg).css('display','none');
                return;
            }

            var visibility = "hidden";
            if(presenter.currentAnimationItem - 1 == i){
                visibility = "visible";
            }

            $(presenter.swiffyContainer).append('&lt;div class="swiffyItem_'+i+' swiffyItem" style="position: absolute; width: 100%; height: 100%;"&gt;&lt;/div&gt;');

            presenter.swiffyItem[i] = $(view).find('.swiffyItem_'+i)[0];
            $(presenter.swiffyItem[i]).css('visibility', visibility);

            if(animation.autoPlay === 'True'){
                presenter.animsRunning[i] = true;
            }else{
                presenter.animsRunning[i] = false;
            }

            if(animation.doNotPreload !== 'True'){
                presenter.animsLoaded[i] = true;

                $.getScript(animation.swiffyobject, function(){
                    presenter.swiffyObject[i] = swiffyobject;
                    presenter.stage[i] = new swiffy.Stage(presenter.swiffyItem[i],presenter.swiffyObject[i]);

                    if(animation.disableTransparentBackground === 'False'){
                        presenter.stage[i].setBackground(null);
                    }

                    if(presenter.animsRunning[i] === true){
                        presenter.stage[i].start();
                    }

                    presenter.animationLoadedDeferred[i+1].resolve();

                });//end getScript
            }

            //Last item, checking if all are loaded
            if(presenter.Animations.length == i+1){
                presenter.checkIfAllAnimationsAreLoaded();
            }

        });//end loop

        //stop propagation
        $(view).find('.swiffyContainer').click(function(e) {
            e.stopImmediatePropagation();
            e.stopPropagation();
        });

        MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.checkIfAllAnimationsAreLoaded = function(){
        presenter.loaded = false;

        var count = 0;
        var last = 1;
        //loop through Animations
        $(presenter.Animations).each(function(i, animation){
            if(presenter.animsLoaded[i]){
                count++;
                last = i+1;
            }
        });

        if(count &gt; 0){
            presenter.animationLoaded[last].then(function() {
                if(presenter.animsLoaded.length == last){
                    presenter.loaded = true;
                    //hide loading icon
                    $(presenter.loadingIconImg).css('display','none');
                }
            });
        }else{
            $(presenter.loadingIconImg).css('display','none');
        }
    }

    presenter.createPreview = function (view, model) {
        //presenter.run(view,model);
        presenter.$view = $(view);

        //ERROR CHECKING
        //incorrect model.initialAnimation number -&gt; error
        if(model.initialAnimation === ""){
            model.initialAnimation = 1;
        } else {
            if( model.initialAnimation &lt; 1 || model.initialAnimation &gt; model.Animations.length || model.initialAnimation%1 !== 0 ){
                presenter.errorHandler("Error: Incorrect Initial Animation number.");
                return;
            }
            model.initialAnimation = parseInt(model.initialAnimation, 10);
        }
        //empty Swiffy Object file -&gt; error
        $(model.Animations).each(function(i, animation){
            if(animation.swiffyobject === ''){
                presenter.errorHandler("Error: Swiffy Object in Item "+(i+1)+" is missing.");
                return;
            }
        });
        var message = "Preview unavailable in editor. Please use Preview button.";
        presenter.$view.prepend('&lt;div style="color: #444444; font-size: 12px; font-family: Arial;"&gt;'+message+'&lt;/div&gt;');
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'loadAnimation': presenter.loadAnimation,
            'start': presenter.start,
            'replay': presenter.replay,
            'setVars': presenter.setVars,
            'switchAnimation': presenter.switchAnimation
        };

        Commands.dispatch(commands, name, params[0], presenter);
    };

    presenter.loadAnimation = function(item){
        var i = typeof item !== 'undefined' ? i = item - 1 : i = presenter.currentAnimationItem - 1;
        var animation = presenter.Animations[i];

        if(typeof presenter.$view.find('.swiffyItem_'+i+' canvas')[0] === 'undefined'){

            $(presenter.loadingIconImg).css('display','block');
            presenter.animsLoaded[i] = true;

            $.getScript(animation.swiffyobject, function(){
                presenter.swiffyObject[i] = swiffyobject;
                presenter.stage[i] = new swiffy.Stage(presenter.swiffyItem[i],presenter.swiffyObject[i]);

                if(animation.disableTransparentBackground === 'False'){
                    presenter.stage[i].setBackground(null);
                }

                if(presenter.animsRunning[i] === true){
                    presenter.stage[i].start();
                }

                presenter.animationLoadedDeferred[i+1].resolve();

                if(presenter.currentAnimationItem - 1 == i){
                    $(presenter.loadingIconImg).css('display','none');
                }

                //stop propagation
                presenter.$view.find('.swiffyContainer').click(function(e){
                    e.stopImmediatePropagation();
                    e.stopPropagation();
                });
            });//end getScript
        }
    };

    presenter.start = function(item){
        var i = typeof item !== 'undefined' ? i = item - 1 : i = presenter.currentAnimationItem - 1;
        if(presenter.animsRunning[i] === false &amp;&amp; typeof presenter.stage[i] !== 'undefined'){
            presenter.stage[i].start();
        }
        if(presenter.animsLoaded[i] == true){
            presenter.animsRunning[i] = true;
        }
    };

    presenter.setVars = function(commands){
        presenter.animationLoaded[presenter.currentAnimationItem].then(function() {
            commands = commands.split(",");
            var i = presenter.currentAnimationItem-1;
            presenter.stage[i].setFlashVars(commands[0]+'='+commands[1]);
        });
    };

    presenter.errorHandler = function(message){
        presenter.$view.prepend('&lt;div style="color: red; font-size: 12px; font-family: Arial;"&gt;'+message+'&lt;/div&gt;');
    };

    presenter.setVisibility = function(isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("display", isVisible ? presenter.originalDisplay : "none");
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        presenter.setVisibility(false);

    };

    presenter.replay = function(item){
        var itemToDestroy = typeof item !== 'undefined' ? itemToDestroy = item - 1 : itemToDestroy = presenter.currentAnimationItem - 1;
        if(presenter.animsRunning[itemToDestroy] === true &amp;&amp; typeof presenter.stage[itemToDestroy] !== 'undefined'){
            var currentSwiffyObject = presenter.swiffyObject[itemToDestroy];
            presenter.swiffyItem = presenter.$view.find('.swiffyItem_'+itemToDestroy)[0];
            presenter.stage[itemToDestroy].destroy();
            presenter.stage[itemToDestroy] = new swiffy.Stage(presenter.swiffyItem,currentSwiffyObject, { });
            if(presenter.Animations[itemToDestroy].disableTransparentBackground === 'False'){
                presenter.stage[itemToDestroy].setBackground(null);
            }
            presenter.stage[itemToDestroy].start();
        }
    };

    presenter.switchAnimation = function(item){
        presenter.currentAnimationItem = item;
        $(presenter.Animations).each(function(i,v){
            if(item == (i+1)){
                $(presenter.$view.find('.swiffyItem_'+i)[0]).css("visibility", "visible");
            }else{
                $(presenter.$view.find('.swiffyItem_'+i)[0]).css("visibility", "hidden");
            }
        });
    };

    presenter.reset = function(){
        if(presenter.loaded === true){
            presenter.loaded = false;
            $(presenter.swiffyContainer).html("");
            $(presenter.Animations).each(function(i, animation){
                if(presenter.animsRunning[i] === true &amp;&amp; typeof presenter.stage[i] !== 'undefined'){
                    presenter.stage[i].destroy();
                }
            });
            $(presenter.loadingIconImg).css('display','block');
            presenter.run(presenter.view,presenter.model);
        }
    };

    presenter.getState = function(){
        return JSON.stringify({
            'currentAnimationItem' : presenter.currentAnimationItem,
            'animsRunning' : presenter.animsRunning,
            'animsLoaded' : presenter.animsLoaded,
            'isVisible' : presenter.isVisible
        });
    };

    presenter.setState = function(state){
        var parsedState = JSON.parse(state);

        presenter.currentAnimationItem = parsedState.currentAnimationItem;
        presenter.switchAnimation(presenter.currentAnimationItem);

        presenter.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.isVisible);

        presenter.animsRunning = parsedState.animsRunning;
        presenter.animsLoaded = parsedState.animsLoaded;

        $(presenter.animsLoaded).each(function(i, loaded){

            if(loaded &amp;&amp; presenter.Animations[i].doNotPreload === 'True'){
                var item = parseInt(i+1,10);
                presenter.loadAnimation(item);
            }
        });

    };

    presenter.destroy = function(event) {
        if (event.target === presenter.view) {
            if (presenter.loaded === true) {
                presenter.loaded = false;
                $(presenter.swiffyContainer).html("");
                $(presenter.Animations).each(function (i, animation) {
                    if (presenter.animsRunning[i] === true &amp;&amp; typeof presenter.stage[i] !== 'undefined') {
                        presenter.stage[i].destroy();
                    }
                });
            }
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Table" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Rows" nameLabel="Table_property_rows" type="string"/>
		<property name="Columns" nameLabel="Table_property_columns" type="string"/>
		<property isDefault="true" name="Table cells" nameLabel="Table_property_table_cells" type="list">
			<property name="Row" nameLabel="Table_property_row" type="string"/>
			<property name="Column" nameLabel="Table_property_column" type="string"/>
			<property isLocalized="true" name="Content" nameLabel="Table_property_content" type="html"/>
            <property name="CSS Class" nameLabel="Table_property_css_class" type="string"/>
            <property name="CSS Style" nameLabel="Table_property_css_style" type="string"/>
		</property>
		<property name="Columns width" nameLabel="Table_property_columns_width" type="list">
			<property name="Width" nameLabel="Table_property_width" type="string"/>
		</property>
		<property name="Rows height" nameLabel="Table_property_rows_height" type="list">
			<property name="Height" nameLabel="Table_property_height" type="string"/>
		</property>
		<property name="Is not an activity" nameLabel="Table_property_is_not_an_activity" type="boolean"/>
		<property name="Is disabled" nameLabel="Table_property_is_disabled" type="boolean"/>
		<property name="Case sensitive" nameLabel="Table_property_case_sensitive" type="boolean"/>
		<property name="useNumericKeyboard" nameLabel="Table_property_use_numeric_keyboard" type="boolean"/>
		<property name="Ignore punctuation" nameLabel="Table_property_ignore_punctuation" type="boolean"/>
		<property name="Gap width" nameLabel="Table_property_gap_width" type="string"/>
        <property name="Gap Type" nameLabel="Table_property_gap_type" type="{editable, draggable, math}"/>
		<property displayName="Gap max length" name="GapMaxLength" nameLabel="Table_property_gap_max_length" type="string"/>
		<property name="newWidthCalculate" nameLabel="Table_property_new_width_calculate" type="boolean"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Table_property_lang_attribute" type="string"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="Hierarchical_Lesson_Report_property_speech_texts" type="staticlist">

            <property name="Correct" nameLabel="choice_item_correct" type="staticrow">
                <property name="Correct" nameLabel="choice_item_correct" type="string"/>
            </property>

			<property name="Wrong" nameLabel="choice_item_wrong" type="staticrow">
                <property name="Wrong" nameLabel="choice_item__wrong" type="string"/>
            </property>

			<property name="Empty" nameLabel="choice_item_empty" type="staticrow">
                <property name="Empty" nameLabel="choice_item_empty" type="string"/>
            </property>

			<property name="Gap" nameLabel="choice_item_gap" type="staticrow">
                <property name="Gap" nameLabel="choice_item_gap" type="string"/>
            </property>

			<property name="Dropdown" nameLabel="choice_item_dropdown" type="staticrow">
                <property name="Dropdown" nameLabel="choice_item_dropdown" type="string"/>
            </property>

			<property name="Inserted" nameLabel="text_insert" type="staticrow">
                <property name="Inserted" nameLabel="text_insert" type="string"/>
            </property>

			<property name="Removed" nameLabel="text_removed" type="staticrow">
                <property name="Removed" nameLabel="text_removed" type="string"/>
            </property>

			<property name="Cell" nameLabel="Table_cell" type="staticrow">
                <property name="Cell" nameLabel="Table_cell" type="string"/>
            </property>

			<property name="Row" nameLabel="Table_row_speech_text" type="staticrow">
				<property name="Row" nameLabel="Table_row_speech_text" type="string"/>
			</property>

			<property name="Column" nameLabel="Table_column_speech_text" type="staticrow">
				<property name="Column" nameLabel="Table_column_speech_text" type="string"/>
			</property>

			<property name="RowSpan" nameLabel="Table_row_span_speech_text" type="staticrow">
				<property name="RowSpan" nameLabel="Table_row_span_speech_text" type="string"/>
			</property>

			<property name="ColSpan" nameLabel="Table_column_span_speech_text" type="staticrow">
				<property name="ColSpan" nameLabel="Table_column_span_speech_text" type="string"/>
			</property>

		</property>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
		<property name="keepOriginalOrder" nameLabel="Table_property_keep_order" type="boolean"/>
		<property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
		<property name="isFirstRowHeader" nameLabel="Table_property_first_row_is_header" type="boolean"/>
		<property name="isFirstColumnHeader" nameLabel="Table_property_first_column_is_header" type="boolean"/>
	</model>
<css>.table-addon-wrapper {
    border: 0;
    padding: 0;
    margin: 0;
    direction: ltr;
    width: 100%;
    height: 100%;
    direction: ltr;
}

.table-addon-wrapper table {
    width: 100%;
    height: 100%;
    border-collapse: collapse;
    vertical-align: middle;
}

.table-addon-wrapper table td {
    padding-left: 10px;
    padding-right: 10px;
    text-align: center;
    vertical-align: middle;
}

.table-addon-wrapper table tr td {
    border: 1px solid black;
}

.table-addon-wrapper .gapFilled{
    cursor: pointer;
}

.table-addon-wrapper .draggable-gap{
    height: 20px;
    width: 40px;
}

/* !important was used because the user's styles obscured this definition and because it does not affect the lesson outside the editor. */
.table-addon-wrapper .cellSelector {
    border: 1px solid grey !important;
}

.printable_addon_Table .table-addon-wrapper {
    height: fit-content;
}

.printable_addon_Table tbody tr:first-child {
    column-break-inside: avoid;
	-webkit-column-break-inside: avoid;
    column-break-after: avoid;
	-webkit-column-break-after: avoid;
}

.printable_addon_Table .table-addon-wrapper .printable_gap {
    border-bottom: 1px solid;
}
.printable_addon_Table .table-addon-wrapper .printable_math_gap {
    margin: 1px 5px;
    padding: 0 5px;
}

.printable_addon_Table .table-addon-wrapper .printable_gap_correct::after {
    content: " \2714";
}

.printable_addon_Table .table-addon-wrapper .printable_gap_wrong::after {
    content: " \2716";
}

.printable_addon_Table.printable_addon_Table-show-answers .table-addon-wrapper table tbody tr td {
    border: 1px black dashed;
}

</css><view>&lt;div class="table-addon-wrapper"&gt;&lt;/div&gt;
</view><preview>&lt;div class="table-addon-wrapper"&gt;&lt;/div&gt;
</preview><presenter>function AddonTable_create() {

    /*
        INTEGRATION WITH MATH MODULE:
            Table supports integration with Math module. Table which is not activity, will show answers provided by
            Math module. Requires implementation of methods and variables listed below.

            Attributes:
                @param isConnectedWith {boolean}  required by Math module, to detect if table is connected with Math

            Methods:
                presenter.setGapAnswer (gapIndex, answer, answersLength) - method used by Math to set gap answer at
                    show answers when table is not activity. Gap index based by DOM occurence order. 1-n based

                presenter.setUserValue (gapIndex, value) - method used by Math module to restore user answer at hide answers

                presenter.getValue (gapIndex) - method used by Math module to get user value in gap by index

                presenter.isActivity - method used by Math module to determine if addon is activity
     */


    var presenter = function () {
    };

    var isConnectedWithMath = false;
    presenter.gapsSize = [];
    presenter.isSetShowErrorsMode = false;
    presenter.keyboardControllerObject = null;
    presenter.isWCAGOn = false;
    presenter.gapNavigation = false;
    presenter.addonKeyboardNavigationActive = false;
    presenter.gapIndex = 0;
    presenter.isGradualShowAnswersActive = false;
    presenter.printableState = null;
    presenter.printableStateMode = null;
    presenter.printableParserID = null;
    presenter.printableParserCallback = null;

    let previewInterval = null;

    presenter.ERROR_CODES = {
        'RW_01': 'Number of rows must be a positive integer!',
        'CL_01': 'Number of columns must be a positive integer!',
        'CO_01': 'Row number must be a positive integer!',
        'CO_02': 'Column number must be a positive integer',
        'CO_03': 'Row number must be a number from 1 to rows count!',
        'CO_04': 'Column number must be a number from 1 to columns count',
        'CO_05': 'Each table cell can be defined only once!',
        'CO_06': 'Empty table cell definition must contain only one empty element!',
        'CR_00': 'Column and row numbers must be sequential within one table cell',
        'CW_01': "Number of items in 'Columns width' property cannot be higher than number of columns!",
        'RH_01': "Number of items in 'Rows height' property cannot be higher than number of rows!",
        'GW_01': "Gap width incorrect!"
    };

    presenter.replaceGapID = function (gap, parsedText) {
        var gapID = presenter.configuration.addonID + gap.id;
        var patt = new RegExp("id=\"" + gap.id + "\"");
        var rep = "id=\"" + gapID + "\"";
        return  parsedText.replace(patt, rep);
    };

    function getParsedHTMLView () {
        let textParserResult = presenter.textParser.parseGaps(presenter.$view.html(),
            {
                isCaseSensitive: presenter.configuration.isCaseSensitive,
                isKeepOriginalOrder: presenter.configuration.keepOriginalOrder,
                useDraggableGaps: presenter.configuration.gapType === "draggable"
            }
        );
        if (presenter.configuration.gapType === "math") {
            textParserResult.parsedText = presenter.textParser.parseMathParentheses(textParserResult.parsedText, true);
        }
        return textParserResult;
    }

    presenter.parseGaps = function (isPreview) {
        if (presenter.configuration.gapType === "draggable") {
            return presenter.parseGapsWrapper(presenter.DraggableDroppableGap, isPreview);
        } else {
            return presenter.parseGapsWrapper(presenter.EditableInputGap, isPreview);
        }
    };

    function changeInlineGapsIDs (inlineGaps, parsedText) {
        inlineGaps.forEach(function (gap) {
            parsedText = presenter.replaceGapID(gap, parsedText);
            var gapID = presenter.configuration.addonID + gap.id;

            presenter.gapsContainer.addGap(new presenter.SelectGap(gapID, [gap.answer], gap.value));
        });

        return parsedText;
    }

    function changeSimpleGapsIDs (simpleGaps, parsedText, objectType) {
        simpleGaps.forEach(function (gap) {
            parsedText = presenter.replaceGapID(gap, parsedText);
            var gapID = presenter.configuration.addonID + gap.id;
            presenter.gapsContainer.addGap(new objectType(gapID, gap.answers, 1));
        });

        return parsedText;
    }

    presenter.parseGapsWrapper = function (objectType, isPreview) {
        var textParserResult = getParsedHTMLView();

        var parsedText = textParserResult.parsedText;
        parsedText = changeSimpleGapsIDs(textParserResult.gaps, parsedText, objectType);
        parsedText = changeInlineGapsIDs(textParserResult.inLineGaps, parsedText, objectType);

        presenter.$view.html(parsedText);

        if(!isPreview){
            presenter.getInputsSize();
        }
    };

    presenter.getInputsSize = function () {
        presenter.$view.find('input').each(function () {
            var inputID = $(this).attr('id'),
                inputSize = $(this).attr('size');

            presenter.gapsSize.push({id : inputID, size: inputSize})
        });
    };

    presenter.setGapsClassAndWidth = function () {
        presenter.$view.find('.ic_inlineChoice').addClass('ic_gap');
        presenter.$view.find('.ic_inlineChoice').css('width', presenter.configuration.gapWidth.value+"px");
    };

    presenter.initializeGaps = function (isPreview) {
        presenter.parseGaps(isPreview);

        if (presenter.hasMathGaps()){
            presenter.gapsContainer.gaps = [];
            $(presenter.$view).find('input').each(function () {
                $(this).replaceWith(
                    generateMathGap(
                        window.xssUtils.sanitize($(this).attr('id')),
                        presenter.configuration.gapWidth.value
                    )
                );
            });
        }

        presenter.gapsContainer.replaceDOMViewWithGap();
        presenter.gapsContainer.updateGapMaxLength();
        presenter.setGapsClassAndWidth();
        presenter.gapsContainer.addMaxLengthParamToGaps();
    };

    /**
     * String generated by this function can be replaced by MathJax to input.
     * See media/js/forminput.js
     * @param id
     * @param width
     */
    function generateMathGap(id, width) {
        return `\\gap{${id}|1|${width}|{{value:${id}}}}`;
    }

    function deleteCommands () {
        delete presenter.getScore;
        delete presenter.getMaxScore;
        delete presenter.getState;
        delete presenter.setState;
        delete presenter.getGapTextCommand;
        delete presenter.getGapTextCommand;
        delete presenter.setGapTextCommand;
        delete presenter.markGapAsEmptyCommand;
        delete presenter.markGapAsCorrectCommand;
        delete presenter.markGapAsWrongCommand;
        delete presenter.enableGapCommand;
        delete presenter.enableAllGaps;
        delete presenter.disableGapCommand;
        delete presenter.disableAllGaps;
        delete presenter.isAttempted;
    }

    function replaceInputsInPreview () {
        if (presenter.configuration.gapType === "draggable") {
            var inputs = presenter.$wrapper.find("input");

            for (var i = 0; i &lt; inputs.length; i++) {
                $(inputs[i]).replaceWith(presenter.DraggableDroppableGap.prototype.createView());
            }
        }
    }

    presenter.logic = function (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.$wrapper = presenter.$view.find('.table-addon-wrapper');
        presenter.isPreview = isPreview;
        presenter.configuration = presenter.validateModel(presenter.upgradeModel(model));

        if (presenter.hasMathGaps()){
            var mathJaxDeferred = new jQuery.Deferred();
            presenter.mathJaxProcessEndedDeferred = mathJaxDeferred;
            presenter.mathJaxProcessEnded = mathJaxDeferred.promise();

            MathJax.Hub.Register.MessageHook("End Process", function (message) {
                if ($(message[1]).hasClass('ic_page')) {
                    presenter.mathJaxProcessEndedDeferred.resolve();
                }
            });
        }

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            deleteCommands();
            return;
        }

        presenter.mainLogic(isPreview);

        if (presenter.hasMathGaps()){
            presenter.mathJaxProcessEnded.then(function() {
                MathJax.CallBack.Queue().Push(function () {
                    if(!isPreview){
                        presenter.renderMathJax();
                        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
                        var checkSelector = setInterval(function () {
                            if ($(presenter.$view).find('input').length &gt; 0) {
                                presenter.gapsContainer.gaps = [];
                                $(presenter.$view).find('input').each(function (_, index) {
                                    for(var i = 0; i &lt; presenter.gapsAnswers.length; i++){
                                        if(presenter.gapsAnswers[i].id === $(this).attr('id')){
                                            var correctAnswers = presenter.gapsAnswers[i].answers;
                                        }
                                    }
                                    presenter.gapsContainer.addGap(new presenter.EditableInputGap($(this).attr('id'), correctAnswers, 1));
                                });
                                clearInterval(checkSelector);
                                presenter.eventBus.sendEvent('ValueChanged', []);
                            }
                        }, 100);
                    }
                });
            });
        }

        if(isPreview) {
            presenter.setEditorGapWidth();
            previewInterval = setInterval(function() { prepareToSelectingCellsInPreview(); }, 500);
        } else {
            presenter.setInputsSize();
        }
    };

    function prepareToSelectingCellsInPreview() {
        const $moduleSelector = $('.moduleSelector[data-id="'+presenter.configuration.addonID+'"]');

        if ($moduleSelector.length &gt; 0) {
            $moduleSelector.on("mousemove", function(e) {
                const mouseX = parseInt(e.pageX, 10);
                const mouseY = parseInt(e.pageY, 10);
                const cell = findCellAtPosition(mouseX, mouseY);
                selectCellInPreview(cell);
            });

            $moduleSelector.on("mouseout", function(e) {
                presenter.$view.find("td").removeClass("cellSelector");
            });

            clearInterval(previewInterval);
        }
    }

    function findCellAtPosition(x, y) {
        const $cells = presenter.$view.find("td");
        for (let i = 0; i &lt; $cells.size(); i++) {
            const cell = $cells.get(i);
            if (!cell) {
                continue;
            }
            const boundingClientReact = cell.getBoundingClientRect();
            if ((boundingClientReact.left &lt; x &amp;&amp; x &lt; boundingClientReact.right) &amp;&amp;
                (boundingClientReact.top &lt; y &amp;&amp; y &lt; boundingClientReact.bottom)
            ) {
                return cell;
            }
        }
        return null;
    }

    function selectCellInPreview(cell) {
        if (!cell) {
            presenter.$view.find("td").removeClass("cellSelector");
            return;
        }
        $(cell).addClass("cellSelector");
        presenter.$view.find("td").not(cell).removeClass("cellSelector");
    }

    presenter.setInputsSize = function () {
        for (var i = 0; i &lt; presenter.gapsSize.length; i++) {
            var inputId = presenter.gapsSize[i].id,
                size = presenter.gapsSize[i].size;

            presenter.$view.find('#'+inputId).attr('size', size);
        }
    };

    presenter.setEditorGapWidth = function () {
        presenter.$view.find('input').css("width", presenter.configuration.gapWidth.value+"px");
        presenter.$view.find('span').css("width", presenter.configuration.gapWidth.value+"px");
    };

    presenter.mainLogic = function (isPreview) {
        presenter.gapsContainer = new presenter.GapsContainerObject();

        var $table = presenter.generateTable(presenter.configuration.contents, isPreview);
        presenter.setColumnWidth($table, presenter.configuration.columnsWidths, presenter.configuration.rowsHeights);
        presenter.setRowHeight($table, presenter.configuration.rowsHeights);
        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        presenter.initializeGaps(isPreview);

        $.each(presenter.$view.find('.table_cell'), function (index, element) {
            const parsedHTML = presenter.textParser.parse($(element).html());
            const sanitizedParsedHTML = window.xssUtils.sanitize(parsedHTML);
            $(element).html(sanitizedParsedHTML);
        });

        if (!isPreview) {
            presenter.parseDefinitionLinks();
        } else {
            replaceInputsInPreview();
            presenter.setGapsClassAndWidth();
            presenter.$view.find('input').attr("size", "auto");
            if (presenter.configuration.gapType === "draggable") {
                presenter.$view.find('input').addClass("draggable-gap");
            }
        }

        presenter.gapsContainer.replaceGapsDOMWithView();
        presenter.lastDraggedItem = {};

        if (presenter.configuration.isDisabledByDefault) {
            presenter.disableAllGaps();
        }

        presenter.buildKeyboardController();
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setEventBus = function (eventBus) {
        presenter.eventBus = eventBus;

        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener('ItemSelected', this);
        presenter.eventBus.addEventListener('ItemConsumed', this);
        presenter.eventBus.addEventListener('GradualShowAnswers', this);
        presenter.eventBus.addEventListener('GradualHideAnswers', this);
    };

    presenter.setPreviewTextParser = function (textParser) {
        presenter.textParser = new TextParserProxy(textParser());
    };

    presenter.getSelectedItem = function () {
        var item = presenter.lastDraggedItem;

        presenter.lastDraggedItem = {};

        return item;
    };

    presenter.createPreview = function (view, model) {
        presenter.logic(view, model, true);
    };

    presenter.run = function (view, model) {
        presenter.logic(view, model, false);
    };

    presenter.reset = function () {
        presenter.gapsContainer.reset();
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.isSetShowErrorsMode = false;
        presenter.attemptedGaps = [];
    };

    presenter.getState = function () {
        var spans;
        var gaps = presenter.gapsContainer.getGapsState();

        if (presenter.configuration.gapType === "draggable") {
            spans = presenter.gapsContainer.getState();
        } else {
            spans = null;
        }

        return JSON.stringify({
            gaps: gaps,
            isVisible: presenter.configuration.isVisible,
            spans: spans
        });
    };

    presenter.setState = function (rawState) {
        var state = JSON.parse(rawState);

        presenter.setVisibility(state.isVisible);
        presenter.configuration.isVisible = state.isVisible;
        presenter.attemptedGaps = state.attemptedGaps === undefined ? presenter.attemptedGaps : state.attemptedGaps;

        if (presenter.hasMathGaps()) {
            var checkSelector = setInterval(
                function () {
                    if ($(presenter.$view).find('.mathGap').length === presenter.gapsAnswers.length) {
                        try{
                            presenter.gapsContainer.setGapsState(state.gaps);
                            presenter.gapsContainer.setSpansState(state.spans);
                            clearInterval(checkSelector);
                        } catch (e) { }
                    }
                }, 100);
        } else {
            presenter.gapsContainer.setGapsState(state.gaps);
            presenter.gapsContainer.setSpansState(state.spans);
        }
    };

    /**
     * Generate table row (tr) element. Each cell has col_C and row_R classes where R is row number
     * (counted from 1 to rows count) and C is column number (from 1 to columns count).
     *
     * @param row row number counted from 0
     * @param content row content array
     * @param isPreview
     *
     * @return {jQuery} jQuery reference to new table row element
     */
    presenter.generateRow = function (row, content, isPreview) {
        var $rowElement = $(document.createElement('tr')), i, length;

        for (i = 0, length = content.length; i &lt; length; i++) {
            if (!content[i]) continue;

            var $element;
            if ((row === 0 &amp;&amp; presenter.configuration.isFirstRowHeader) ||
                (i === 0 &amp;&amp; presenter.configuration.isFirstColumnHeader)) {
                $element = $(document.createElement('th'));
            } else {
                $element = $(document.createElement('td'));
            }

            $element.addClass('table_cell');
            $element.addClass('row_' + (row + 1));
            $element.addClass('col_' + (i + 1));
            $element.html(content[i].content);
            const elementAttributes = {
                colspan: content[i].colSpan,
                rowspan: content[i].rowSpan
            };
            if (isPreview) {
                elementAttributes.tableCellConfIndex = content[i].tableCellConfIndex;
            }
            $element.attr(elementAttributes);

            if ( presenter.configuration.isTabindexEnabled) {
                $element.attr('tabindex', '0');
            }

            if (content[i].class) {
                $element.addClass(content[i].class)
            }
            if (content[i].style) {
                $element.attr({style:content[i].style})
            }

            $rowElement.append($element);
        }

        return $rowElement;
    };

    presenter.parseDefinitionLinks = function () {
        presenter.textParser.connectLinks(presenter.$view);
    };

    /**
     * Generate table element with content based on provided array.
     *
     * @param content array with table cells contents
     * @param isPreview
     *
     * @return {jQuery} jQuery reference to new table element
     */
    presenter.generateTable = function (content, isPreview) {
        var $table = $(document.createElement('table'));

        for (var i = 0, length = content.length; i &lt; length; i++) {
            var $row = presenter.generateRow(i, content[i], isPreview);

            $table.append($row);
        }

        presenter.$wrapper.html($table);

        return $table;
    };

    presenter.setColumnWidth = function ($table, columnWidth, rowsHeights) {
        var i = 0;
        if (presenter.configuration.newWidthCalculate) {
            var rowsNumber = rowsHeights.length;
            var columsNumber = columnWidth.length;
            for (var row = 1; row &lt;= rowsNumber; row++) {
                var foundedRow = $table.find('.row_' + row);
                for (i = 0; i &lt; columsNumber; i++) {
                    $(foundedRow[i]).css('width', columnWidth[i]);
                }
            }
        } else {
            var firstRow = $table.find('.row_1');

            for (i = 0; i &lt; columnWidth.length; i++) {
                $(firstRow[i]).css('width', columnWidth[i]);
            }
        }
};


    presenter.setRowHeight = function ($table, rowHeight) {
        var i;
        for (i = 0; i &lt; rowHeight.length; i++) {
            $table.find('.row_' + (i + 1)).each(function () {
                $(this).css('height', rowHeight[i]);
            });
        }
    };

    /**
     * Validate content list and convert it into multidimensional {Array}. Not specified contents equals empty elements.
     *
     * @param content list of content definition (consist Row, Column and Content fields)
     * @param rowsCount number of rows
     * @param columnsCount number of columns
     *
     * @return {Object} validation result
     * @return {Boolean} isValid
     * @return {String} error code if any occurs
     * @return {Array} contents array of contents. Dimensions based on Rows and Columns properties
     */
    presenter.validateContent = function (content, rowsCount, columnsCount) {
        var validatedContent = [], controlArray = [], c, r;

        for (r = 0; r &lt; rowsCount; r++) {
            validatedContent[r] = [];
            controlArray[r] = [];

            for (c = 0; c &lt; columnsCount; c++) {
                validatedContent[r][c] = { content: "", rowSpan: 1, colSpan: 1 };
                controlArray[r][c] = false;
            }
        }

        if (ModelValidationUtils.isArrayElementEmpty(content[0])) {
            if (content.length === 1) {
                return { isValid: true, content: validatedContent };
            } else {
                return { isValid: false, errorCode: 'CO_06' };
            }
        }

        for (var i = 0, length = content.length; i &lt; length; i++) {
            var rows = presenter.validateSequence(content[i].Row, rowsCount, true);
            if (!rows.isValid)  return { isValid: false, errorCode: rows.errorCode };

            var columns = presenter.validateSequence(content[i].Column, columnsCount, false);
            if (!columns.isValid)  return { isValid: false, errorCode: columns.errorCode };

            for (r = 0; r &lt; rows.values.length; r++) {
                for (c = 0; c &lt; columns.values.length; c++) {
                    var row = rows.values[r] - 1;
                    var column = columns.values[c] - 1;

                    if (controlArray[row][column]) return { isValid: false, errorCode: 'CO_05' };
                    controlArray[row][column] = true;

                    if (r === 0 &amp;&amp; c === 0) {
                        validatedContent[row][column] = {
                            content: content[i].Content,
                            rowSpan: rows.values.length,
                            colSpan: columns.values.length,
                            class : content[i].hasOwnProperty("CSS Class") ? content[i]["CSS Class"] : "",
                            style: content[i].hasOwnProperty("CSS Style") ? content[i]["CSS Style"] : ""
                        };
                        if (presenter.isPreview) {
                            validatedContent[row][column].tableCellConfIndex = i;
                        }
                    } else {
                        validatedContent[row][column] = undefined;
                    }
                }
            }
        }

        return { isValid: true, content: validatedContent };
    };

    presenter.validateSingleNumber = function (column, columnsCount, isRowValidated) {
        var validatedColumn = ModelValidationUtils.validatePositiveInteger(column);

        if (!validatedColumn.isValid) return { isValid: false, errorCode: isRowValidated ? 'CO_01' : 'CO_02'};
        if (validatedColumn.value &gt; columnsCount) return { isValid: false, errorCode: isRowValidated ? 'CO_03' : 'CO_04' };

        return {isValid: true, value: validatedColumn.value };
    };

    presenter.validateSequence = function (columns, columnsCount, isRowValidated) {
        var values = [], validatedColumn, splittedColumns, i;

        splittedColumns = columns.indexOf(',') === -1 ? [columns] : columns.split(',');

        for (i = 0; i &lt; splittedColumns.length; i++) {
            validatedColumn = presenter.validateSingleNumber(splittedColumns[i], columnsCount, isRowValidated);

            if (!validatedColumn.isValid) return { isValid: false, errorCode: validatedColumn.errorCode };

            values.push(validatedColumn.value);
        }

        if (values.length &gt; 1) {
            for (i = 1; i &lt; values.length; i++) {
                if (values[i - 1] + 1 !== values[i]) {
                    return { isValid: false, errorCode: 'CR_00' };
                }
            }
        }

        return { values: values, isValid: true };
    };

    presenter.convertDimensionsArray = function (columnWidth, columnsCount, propertyName) {
        if (columnWidth.length &gt; columnsCount) return { isValid: false };

        var widths = [], value, i;
        for (i = 0; i &lt; columnWidth.length; i++) {
            if (ModelValidationUtils.isStringEmpty(columnWidth[i][propertyName])) {
                value = 'auto';
            } else {
                value = columnWidth[i][propertyName];
            }

            widths.push(value);
        }

        if (columnWidth.length &lt; columnsCount) {
            for (i = columnWidth.length; i &lt; columnsCount; i++) {
                widths.push('auto');
            }
        }

        return { isValid: true, dimensions: widths };
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            correct:  'correct',
            wrong: 'wrong',
            empty: 'empty',
            gap: 'gap',
            dropdown: 'dropdown',
            inserted: 'inserted',
            removed: 'removed',
            cell: 'cell',
            row: 'row',
            column: 'column',
            rowSpan: 'row span',
            colSpan: 'column span'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            correct:    getSpeechTextProperty(speechTexts['Correct']['Correct'], presenter.speechTexts.correct),
            wrong: getSpeechTextProperty(speechTexts['Wrong']['Wrong'], presenter.speechTexts.wrong),
            empty:  getSpeechTextProperty(speechTexts['Empty']['Empty'], presenter.speechTexts.empty),
            gap:     getSpeechTextProperty(speechTexts['Gap']['Gap'], presenter.speechTexts.gap),
            dropdown:   getSpeechTextProperty(speechTexts['Dropdown']['Dropdown'], presenter.speechTexts.dropdown),
            inserted:      getSpeechTextProperty(speechTexts['Inserted']['Inserted'], presenter.speechTexts.inserted),
            removed:        getSpeechTextProperty(speechTexts['Removed']['Removed'], presenter.speechTexts.removed),
            cell:        getSpeechTextProperty(speechTexts['Cell']['Cell'], presenter.speechTexts.cell),
            row:        getSpeechTextProperty(speechTexts['Row']['Row'], presenter.speechTexts.row),
            column:        getSpeechTextProperty(speechTexts['Column']['Column'], presenter.speechTexts.column),
            rowSpan:        getSpeechTextProperty(speechTexts['RowSpan']['RowSpan'], presenter.speechTexts.rowSpan),
            colSpan:        getSpeechTextProperty(speechTexts['ColSpan']['ColSpan'], presenter.speechTexts.colSpan)
        };
    };

    /**
     * Validate user input configuration.
     *
     * In iceditor, there is an UpdatePropertyOfTableAddonAction action, responsible for automatically
     * enlarging/reducing the list of `Table cells` when the value in 'Rows' or 'Columns' is changed.
     * When changing the validation logic here, please correct that validation with the appropriate change if it
     * applies to UpdatePropertyOfTableAddonAction functionality.
     *
     * @param model {Array} of properties injected into Addon
     *
     * @return {Object} validation result
     * @return {Boolean} isValid
     * @return {String} error code if any occurs
     * @return {Object} contents array of contents. Dimensions based on Rows and Columns properties
     */
    presenter.validateModel = function (model) {

        presenter.setSpeechTexts(model['speechTexts']);

        if (model["newWidthCalculate"] === undefined) {
            model["newWidthCalculate"] = false;
        }

        var validatedRows = ModelValidationUtils.validatePositiveInteger(model.Rows);
        if (!validatedRows.isValid) {
            return { isValid: false, errorCode: 'RW_01' };
        }

        var validatedColumns = ModelValidationUtils.validatePositiveInteger(model.Columns);
        if (!validatedColumns.isValid) {
            return { isValid: false, errorCode: 'CL_01' };
        }

        var validatedContents = presenter.validateContent(model["Table cells"], validatedRows.value, validatedColumns.value);
        if (!validatedContents.isValid) {
            return { isValid: false, errorCode: validatedContents.errorCode };
        }

        var convertedColumnWidth = presenter.convertDimensionsArray(model["Columns width"], validatedColumns.value, 'Width');
        if (!convertedColumnWidth.isValid) {
            return { isValid: false, errorCode: 'CW_01' };
        }

        var convertedRowWidths = presenter.convertDimensionsArray(model["Rows height"], validatedRows.value, 'Height');
        if (!convertedRowWidths.isValid) {
            return { isValid: false, errorCode: 'RH_01' };
        }

        var gapWidth = { isSet: false, value: undefined };
        if (!ModelValidationUtils.isStringEmpty(model["Gap width"])) {
            var validatedGapWidth = ModelValidationUtils.validatePositiveInteger(model["Gap width"]);
            if (!validatedGapWidth.isValid) {
                return { isValid: false, errorCode: 'GW_01' };
            } else {
                gapWidth = { isSet: true, value: validatedGapWidth.value };
            }
        }

        var isFirstRowHeader = ModelValidationUtils.validateBoolean(model["isFirstRowHeader"]);
        var isFirstColumnHeader = ModelValidationUtils.validateBoolean(model["isFirstColumnHeader"]);

        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']);

        const gapMaxLength = ModelValidationUtils.validatePositiveInteger(model["GapMaxLength"]);

        return {
            addonID: model.ID,
            isValid: true,
            contents: validatedContents.content,
            columnsWidths: convertedColumnWidth.dimensions,
            rowsHeights: convertedRowWidths.dimensions,
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isActivity: !ModelValidationUtils.validateBoolean(model["Is not an activity"]),
            isNotActivity: ModelValidationUtils.validateBoolean(model["Is not an activity"]),
            isDisabledByDefault: ModelValidationUtils.validateBoolean(model["Is disabled"]),
            isPunctuationIgnored: ModelValidationUtils.validateBoolean(model["Ignore punctuation"]),
            isCaseSensitive: ModelValidationUtils.validateBoolean(model["Case sensitive"]),
            newWidthCalculate: ModelValidationUtils.validateBoolean(model["newWidthCalculate"]),
            gapWidth: gapWidth,
            gapType: model["Gap Type"],
            gapMaxLength: gapMaxLength,
            isTabindexEnabled: isTabindexEnabled,
            columnsCount: validatedColumns.value,
            rowsCount: validatedRows.value,
            langTag: model["langAttribute"],
            useNumericKeyboard: ModelValidationUtils.validateBoolean(model["useNumericKeyboard"]),
            keepOriginalOrder: ModelValidationUtils.validateBoolean(model["keepOriginalOrder"]),
            isFirstRowHeader: isFirstRowHeader,
            isFirstColumnHeader: isFirstColumnHeader
        };
    };

    presenter.addColumnsWidth = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model["Columns width"]) {
            upgradedModel["Columns width"] = [
                { Width: "" }
            ];
        }

        return upgradedModel;
    };

    presenter.addRowHeights = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model["Rows height"]) {
            upgradedModel["Rows height"] = [
                { Height: "" }
            ];
        }

        return upgradedModel;
    };

    presenter.addLangTag = function AddonTable_addLangTag(model) {
         var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['langAttribute']) {
            upgradedModel['langAttribute'] =  '';
        }

        return upgradedModel;
    };

    presenter.addSpeechTexts = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!model['speechTexts']) {
            upgradedModel['speechTexts'] = {
                Gap: {Gap: "Gap"},
                Dropdown: {Dropdown: "Dropdown"},
                Correct: {Correct: "Correct"},
                Wrong: {Wrong: "Wrong"},
                Empty: {Empty: "Empty"},
                Inserted: {Inserted: "Inserted"},
                Removed: {Removed: "Removed"},
                Cell: {Cell: "Cell"}
            }
        }
        return upgradedModel;
    };

    presenter.addUseNumericKeyboard = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.useNumericKeyboard === undefined) {
            upgradedModel["useNumericKeyboard"] = "False";
        }

        return upgradedModel;
    };

    presenter.addKeepOriginalOrder = function(model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.keepOriginalOrder === undefined) {
            upgradedModel["keepOriginalOrder"] = "False";
        }

        return upgradedModel;
    };

    presenter.addHeaders = function(model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (model.isFirstRowHeader === undefined) {
            upgradedModel["isFirstRowHeader"] = "False";
        }

        if (model.isFirstColumnHeader === undefined) {
            upgradedModel["isFirstColumnHeader"] = "False";
        }

        if (model['speechTexts']["Row"] === undefined) {
            upgradedModel['speechTexts']["Row"] = {Row: "Row"};
        }

        if (model['speechTexts']["Column"] === undefined) {
            upgradedModel['speechTexts']["Column"] = {Column: "Column"};
        }

        return upgradedModel;
    };

    presenter.addSpanSpeechTexts = function(model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (model['speechTexts']["RowSpan"] === undefined) {
            upgradedModel['speechTexts']["RowSpan"] = {RowSpan: "Row span"};
        }

        if (model['speechTexts']["ColSpan"] === undefined) {
            upgradedModel['speechTexts']["ColSpan"] = {ColSpan: "Column span"};
        }

        return upgradedModel;
    };

    presenter.addGapMaxLength = function (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('GapMaxLength')) {
            upgradedModel['GapMaxLength'] = '0';
        }

        return upgradedModel;
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.addColumnsWidth(model);
        upgradedModel = presenter.addRowHeights(upgradedModel);
        upgradedModel = presenter.addLangTag(upgradedModel);
        upgradedModel = presenter.addSpeechTexts(upgradedModel);
        upgradedModel = presenter.addUseNumericKeyboard(upgradedModel);
        upgradedModel = presenter.addKeepOriginalOrder(upgradedModel);
        upgradedModel = presenter.addHeaders(upgradedModel);
        upgradedModel = presenter.addSpanSpeechTexts(upgradedModel);
        upgradedModel = presenter.addGapMaxLength(upgradedModel);
        return upgradedModel;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
        presenter.enableAllGaps();
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
        presenter.disableAllGaps();
    };

    presenter.getGapText = function (gapIndex) {
        return executeFunctionOnGap(gapIndex, "getGapValueByIndex");
    };

    presenter.getGapValue = function (gapIndex) {
        return presenter.getGapText(gapIndex);
    };

    presenter.getGapTextCommand = function (params) {
        return presenter.getGapText(parseInt(params[0], 10));
    };

    presenter.setGapText = function (gapIndex, text) {
        if (["draggable", "math"].includes(presenter.configuration.gapType)) {
            return;
        }

        const validatedGapIndex = presenter.validateGapIndex(gapIndex);
        if (validatedGapIndex.isValid) {
            presenter.gapsContainer.setGapTextByIndex(validatedGapIndex.index, text);
        }
    };

    presenter.setGapTextCommand = function (params) {
        presenter.setGapText(parseInt(params[0], 10), params[1]);
    };

    function executeFunctionOnGap(gapIndex, functionToCall) {
        var validatedGapIndex = presenter.validateGapIndex(gapIndex);

        if (validatedGapIndex.isValid) {
            return presenter.gapsContainer[functionToCall](validatedGapIndex.index);
        }
    }

    presenter.validateGapIndex = function (gapIndex) {
        if (isNaN(gapIndex) || gapIndex &lt; 1 || gapIndex &gt; presenter.gapsContainer.getLength()) {
            return {isValid: false};
        }

        return {
            isValid: true,
            index: parseInt(gapIndex, 10) - 1
        };
    };


    presenter.markGapAsCorrect = function (gapIndex) {
        executeFunctionOnGap(gapIndex, "markGapByIndexAsCorrect");
    };

    presenter.markGapAsCorrectCommand = function (params) {
        presenter.markGapAsCorrect(parseInt(params[0], 10));
    };

    presenter.markGapAsWrong = function (gapIndex) {
        executeFunctionOnGap(gapIndex, "markGapByIndexAsWrong");
    };

    presenter.markGapAsWrongCommand = function (params) {
        presenter.markGapAsWrong(parseInt(params[0], 10));
    };

    presenter.markGapAsEmpty = function (gapIndex) {
        executeFunctionOnGap(gapIndex, "markGapByIndexAsEmpty");
    };

    presenter.markGapAsEmptyCommand = function (params) {
        presenter.markGapAsEmpty(parseInt(params[0], 10));
    };

    presenter.enableGap = function (gapIndex) {
        executeFunctionOnGap(gapIndex, "enableGapByIndex");
    };

    presenter.enableGapCommand = function (params) {
        presenter.enableGap(parseInt(params[0], 10));
    };

    presenter.enableAllGaps = function () {
        presenter.gapsContainer.enableAllGaps();
    };

    presenter.disableGap = function (gapIndex) {
        executeFunctionOnGap(gapIndex, "disableGapByIndex");
    };

    presenter.disableGapCommand = function (params) {
        presenter.disableGap(parseInt(params[0], 10));
    };

    presenter.disableAllGaps = function () {
        presenter.gapsContainer.disableAllGaps();
    };

    presenter.getView = function() {
        return presenter.$view;
    };

    presenter.isAllOK = function() {
        var score = presenter.getScore();
        return score === presenter.getMaxScore() &amp;&amp; score !== 0;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'disableAllGaps': presenter.disableAllGaps,
            'disableGap': presenter.disableGapCommand,
            'enableAllGaps': presenter.enableAllGaps,
            'enableGap': presenter.enableGapCommand,
            'getGapText': presenter.getGapTextCommand,
            'getGapValue': presenter.getGapTextCommand,
            'setGapText': presenter.setGapTextCommand,
            'getView': presenter.getView,
            'hide': presenter.hide,
            'isAllOK': presenter.isAllOK,
            'isAttempted': presenter.isAttempted,
            'markGapAsCorrect': presenter.markGapAsCorrectCommand,
            'markGapAsEmpty': presenter.markGapAsEmptyCommand,
            'markGapAsWrong': presenter.markGapAsWrongCommand,
            'show': presenter.show
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.sendAllOKEvent = function () {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        if (presenter.gapsContainer === undefined) {
            return 0;
        }

        return presenter.gapsContainer.getMaxScore();
    };

    presenter.getScore = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        if (presenter.gapsContainer === undefined) {
            return 0;
        }

        return presenter.gapsContainer.getScore();
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        if (presenter.gapsContainer === undefined) {
            return 0;
        }

        return presenter.gapsContainer.getErrorCount();
    };

    presenter.setShowErrorsMode = function () {
        if (!presenter.isSetShowErrorsMode) {
            presenter.gapsContainer.check();
            presenter.isSetShowErrorsMode = true;
        }

        if (isConnectedWithMath) {
            if (presenter.isShowAnswersActive) {
                presenter.gapsContainer.gaps.forEach(function(gap) {
                    gap.onHideAnswers();
                })
            }
            presenter.gapsContainer.lockAllGaps();
        }
    };

    presenter.setWorkMode = function () {
        if (presenter.isSetShowErrorsMode) {
            presenter.gapsContainer.check();
            presenter.isSetShowErrorsMode = false;
        }

        presenter.gapsContainer.removeAllGapsClasses();

        if (isConnectedWithMath) {
            presenter.gapsContainer.unlockAllGaps();
        }
    };

    presenter.createEventData = function (item, value, score) {
        return {
            source: presenter.configuration.addonID,
            item: "" + item,
            value: "" + value,
            score: "" + score
        };
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;
            case "HideAnswers":
                presenter.hideAnswers();
                break;
            case "GradualShowAnswers":
                if (!presenter.isGradualShowAnswersActive) {
                    presenter.isGradualShowAnswersActive = true;
                }
                if (eventData.moduleID === presenter.configuration.addonID) {
                    presenter.gradualShowAnswers(parseInt(eventData.item, 10));
                }
                break;
            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
            case "ItemSelected":
                if(presenter.configuration.isVisible) {
                    presenter.lastDraggedItem = eventData;
                }
                break;
            case "ItemConsumed":
                const isEqualToDraggedValue = presenter.lastDraggedItem.value &amp;&amp; eventData.value === presenter.lastDraggedItem.value
                const isEqualToDraggedItem = presenter.lastDraggedItem.item &amp;&amp; eventData.item === presenter.lastDraggedItem.item

                if (isEqualToDraggedValue  &amp;&amp; isEqualToDraggedItem &amp;&amp; presenter.configuration.isVisible) {
                    presenter.lastDraggedItem = {};
                }
                break;
            default:
                break;
            
        }
    };

    presenter.showAnswers = function () {
        if (presenter.configuration.isActivity) {
            presenter.gapsContainer.showAnswers();
            presenter.isShowAnswersActive = true;
            presenter.isSetShowErrorsMode = false;
            presenter.renderMathJax();
        } else {
            if (presenter.isSetShowErrorsMode) {
                presenter.setWorkMode();

                if (isConnectedWithMath) {
                    presenter.gapsContainer.lockAllGaps();
                }
            }
            if (isConnectedWithMath) {
                presenter.isShowAnswersActive = true;
                presenter.isSetShowErrorsMode = false;
            }
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        if (presenter.configuration.isActivity || isConnectedWithMath) {
            presenter.gapsContainer.hideAnswers();
            if (isConnectedWithMath){
                presenter.gapsContainer.unlockAllGaps();
            }
            presenter.isShowAnswersActive = false;
            presenter.isSetShowErrorsMode = false;
            presenter.renderMathJax();
        }
    };

    presenter.getCSSConfiguration = function () {
        return {
            correct: "ic_gap-correct",
            wrong: "ic_gap-wrong",
            showAnswers: "ic_gap-show-answers",
            block: "ic_gap-empty",
            droppableHover: "table-addon-gap-highlight"
        };
    };

    presenter.GapUtils = function (configuration) {
        if(!presenter.isPreview){
            DraggableDroppableObject.call(this, configuration, presenter.getCSSConfiguration());
        }

        this.gapScore = configuration.gapScore;
        this.gapType = presenter.GapUtils.GAP_TYPE.NORMAL;
        this.isDisabled = false;
        this.isEnabled = true;
        this.mathShowAnswersValue = "";
        this.mathCSSClass = "math-answer";
        this.valueChangeObserver = new presenter.ValueChangeObserver();
        this.isAttempted = false;
    };

    presenter.GapUtils.prototype = Object.create(DraggableDroppableObject.prototype);
    presenter.GapUtils.parent = DraggableDroppableObject.prototype;

    presenter.GapUtils.GAP_TYPE = {
        NORMAL: 0,
        SELECT: 1
    };

    presenter.GapUtils.prototype.getParsedCorrectAnswers = function () {
        return this.showAnswersValue.map(function (value) {
            return this.parseValue(value);
        }, this);
    };

    presenter.GapUtils.prototype.getParsedValue = function () {
        return this.parseValue(this.getValue());
    };

    presenter.GapUtils.prototype.isCorrect = function () {
        var correctAnswers = this.getParsedCorrectAnswers();
        var value = this.getParsedValue();

        return (correctAnswers.indexOf(value) !== -1);
    };

    presenter.GapUtils.prototype.parseValue = function (value) {
        if(presenter.hasMathGaps()) {
            return value;
        }

        value = presenter.textParser.parseAnswer(value);
        if (!presenter.configuration.isCaseSensitive) {
            value = value.toLowerCase();
        }

        if (presenter.configuration.isPunctuationIgnored) {
            value = value.replace(/\W/g, '');
        }

        return value.trim();
    };

    presenter.GapUtils.prototype.isValueEmpty = function () {
        var value = this.getValue().trim();
        return value.length === 0 || (this.gapType===1 &amp;&amp; 0 === value.localeCompare("---")); //gapType===1 is the dropdown gap
    };

    presenter.GapUtils.prototype.setCssOnCorrect = function () {
        if (presenter.configuration.isNotActivity) {
            return true;
        }

        DraggableDroppableObject.prototype.setCssOnCorrect.call(this);
    };

    presenter.GapUtils.prototype.setCssOnUnCorrect = function () {
        this.removeAllClasses();
    };

    presenter.GapUtils.prototype.onBlock = function () {
        if (!this.isDisabled) {
            this.lock();
        }
    };

    presenter.GapUtils.prototype.onUnblock = function () {
        if (this.isDisabled &amp;&amp; this.isEnabled) {
            this.unlock();
        }
    };

    presenter.GapUtils.prototype.onCorrect = function () {
        this.onBlock();
    };

    presenter.GapUtils.prototype.onUnCorrect = function () {
        this.onUnblock();
    };

    presenter.GapUtils.prototype.onWrong = function () {
        this.onBlock();
    };

    presenter.GapUtils.prototype.onUnWrong = function () {
        this.onUnblock();
    };

    presenter.GapUtils.prototype.setCssOnWrong = function () {
        if (presenter.configuration.isNotActivity) {
            return true;
        }

        if (this.isValueEmpty()) {
            this.addCssClass('ic_gap-empty');
            return;
        }

        DraggableDroppableObject.prototype.setCssOnWrong.call(this);
    };

    presenter.GapUtils.prototype.setCssOnUnWrong = function () {
        this.removeAllClasses();
    };

    presenter.GapUtils.prototype.setCssOnShowAnswers = function () {
        if (presenter.configuration.isActivity || isConnectedWithMath) {
            DraggableDroppableObject.prototype.setCssOnShowAnswers.call(this);
        }

        if (isConnectedWithMath) {
            this.addCssClass(this.mathCSSClass);
        }
    };

    presenter.GapUtils.prototype.setCssOnHideAnswers = function () {
        if (presenter.configuration.isActivity || isConnectedWithMath) {
            DraggableDroppableObject.prototype.setCssOnHideAnswers.call(this);
    	}

        if (isConnectedWithMath) {
            this.removeCssClass(this.mathCSSClass);
        }
    };

    presenter.GapUtils.prototype.onShowAnswers = function () {
        if (presenter.configuration.isActivity) {
            this.onBlock();
            this.setViewValue(this.showAnswersValue[0]);
        }

        if (presenter.configuration.gapType === "draggable" &amp;&amp; this.$view) {
            this.$view.html(presenter.textParser.parseAltTexts(this.$view.html()));
        }

        if (isConnectedWithMath) {
            this.onBlock();
            this.setViewValue(this.mathShowAnswersValue);
        }
    };

    presenter.GapUtils.prototype.onHideAnswers = function () {
        if (presenter.configuration.isActivity || isConnectedWithMath) {
            this.setViewValue(this.value);
            this.onUnblock();
        }
        if (presenter.configuration.gapType === "draggable" &amp;&amp; this.$view) {
            this.$view.html(presenter.textParser.parseAltTexts(this.$view.html()));
        }
    };

    presenter.GapUtils.prototype.setGapWidth = function () {
        if (presenter.configuration.gapWidth.isSet &amp;&amp; !presenter.isPreview) {
            this.$view.width(presenter.configuration.gapWidth.value + 'px');
        }
    };

    presenter.GapUtils.prototype.removeAllClasses = function () {
        this.removeCssClass('ic_gap-correct');
        this.removeCssClass('ic_gap-wrong');
        this.removeCssClass('ic_gap-empty');
    };

    presenter.GapUtils.prototype.getScore = function () {
        return this.isCorrect() ? this.gapScore : 0;
    };

    presenter.GapUtils.prototype.getErrorCount = function () {
        if (this.getValue().trim() === "") {
            return 0;
        }

        return this.isCorrect() ? 0 : 1;
    };

    presenter.GapUtils.prototype.notify = function () {
        if (this.isValueEmpty()) {
            this.isAttempted = false;
        } else {
            this.isAttempted = true;
        }

        this.valueChangeObserver.notify(this.getValueChangeEventData());
    };

    /**
     *
     * @returns {{objectID: (string|*), isCorrect: boolean, value: string}}
     */
    presenter.GapUtils.prototype.getValueChangeEventData = function () {
        return {
            objectID: this.getObjectID(),
            isCorrect: this.isCorrect(),
            value: this.getValue()
        };
    };

    presenter.GapUtils.prototype.setIsEnabled = function (value) {
        if (this.isDisabled &amp;&amp; value) {
            this.unlock();
        }

        if (!this.isDisabled &amp;&amp; !value) {
            this.lock();
        }

        this.isEnabled = value;
    };

    presenter.GapUtils.prototype.setIsLocked = function (value) {
        if (this.isEnabled &amp;&amp; this.isDisabled &amp;&amp; !value) {
            this.unlock();
        }

        if (!this.isDisabled &amp;&amp; value) {
            this.lock();
        }
    };

    presenter.GapUtils.prototype.setMathShowAnswersValue = function (value) {
        this.mathShowAnswersValue = value;
    };

    presenter.GapUtils.prototype.getGapState = function () {
        return {
            isAttempted: this.isAttempted,
            isEnabled: this.isEnabled,
            value: this.getValue()
        };
    };

    presenter.GapUtils.prototype.getState = function () {
        return {
            droppedElement: this.getDroppedElement(),
            isAttempted: this.isAttempted,
            item: this.getSource(),
            value: this.getValue()
        };
    };

    presenter.GapUtils.prototype.setState = function (configuration) {
        var value = configuration.value;
        var source = configuration.source;
        var isEnabled = configuration.isEnabled;
        var droppedElement = configuration.droppedElement;
        this.isAttempted = configuration.isAttempted === undefined ? false : configuration.isAttempted;

        if (presenter.configuration.gapType === "draggable") {
            DraggableDroppableObject.prototype.setState.call(this, value, source, droppedElement);
            if (this.$view) {
                this.$view.html(presenter.textParser.parseAltTexts(this.$view.html()));
            }
        } else {
            this.value = value;
            this.source = source;
            this.setViewValue(value);
        }

        if (isEnabled !== undefined) {
            this.setIsEnabled(isEnabled);
        }
    };

    presenter.GapUtils.prototype.onReset = function () {
        DraggableDroppableObject.prototype.onReset.call(this);

        if (presenter.configuration.isDisabledByDefault) {
            this.setIsEnabled(false);
        } else {
            this.setIsEnabled(true);
        }

        this.removeAllClasses();
        this.removeCssClass("gapFilled");
        this.isAttempted = false;
    };

    presenter.GapUtils.prototype.setAttempted = function (value) {
        this.isAttempted = value;
    };


    presenter.SelectGap = function (htmlID, correctAnswer, gapScore) {
        var configuration = {
            addonID: presenter.configuration.addonID,
            objectID: htmlID,
            eventBus: presenter.eventBus,
            getSelectedItem: presenter.getSelectedItem,

            showAnswersValue: correctAnswer,

            connectEvents: presenter.SelectGap.prototype.connectEvents,
            setViewValue: presenter.SelectGap.prototype.setViewValue,

            gapScore: gapScore,
            maxLength: presenter.getMaxLength(correctAnswer)
        };

        presenter.GapUtils.call(this, configuration);

        this.gapType = presenter.GapUtils.GAP_TYPE.SELECT;
        this.setGapWidth();
    };

    presenter.SelectGap.prototype = Object.create(presenter.GapUtils.prototype);
    presenter.SelectGap.constructor = presenter.SelectGap;

    presenter.SelectGap.prototype.connectEvents = function () {
        this.$view.off('change').bind('change', this.onEdit.bind(this));
    };

    presenter.SelectGap.prototype.onEdit = function (event) {
        this.notifyEdit();
        this.value = this.$view.find(":selected").text();
        this.notify();
    };

    presenter.SelectGap.prototype.setViewValue = function (value) {
        var escaped = presenter.textParser.escapeXMLEntities(value);
        this.$view.val(escaped);
    };

    presenter.SelectGap.prototype.lock = function () {
        this.isDisabled = true;
        this.$view.attr('disabled','disabled');
    };

    presenter.SelectGap.prototype.unlock = function () {
        this.isDisabled = false;
        this.$view.removeAttr('disabled');
    };

    presenter.gapsAnswers = [];

    function addGapAnswers(htmlID, correctAnswer) {
        var isInTable = false;
        for (var i = 0; i &lt; presenter.gapsAnswers.length; i++){
            if(presenter.gapsAnswers[i].id === htmlID){
                isInTable = true;
            }
        }

        if(!isInTable){
            presenter.gapsAnswers.push({id: htmlID, answers: correctAnswer});
        }
    }

    presenter.EditableInputGap = function (htmlID, correctAnswer, gapScore) {
        addGapAnswers(htmlID, correctAnswer);
        var configuration = {
            addonID: presenter.configuration.addonID,
            objectID: htmlID,
            eventBus: presenter.eventBus,
            getSelectedItem: presenter.getSelectedItem,

            showAnswersValue: correctAnswer,

            createView: presenter.EditableInputGap.prototype.createView,
            connectEvents: this.connectEvents,
            setViewValue: presenter.EditableInputGap.prototype.setViewValue,

            gapScore: gapScore,
            maxLength: presenter.getMaxLength(correctAnswer)
        };

        presenter.GapUtils.call(this, configuration);
        this.setGapWidth();
    };

    presenter.EditableInputGap.prototype = Object.create(presenter.GapUtils.prototype);
    presenter.EditableInputGap.constructor = presenter.EditableInputGap;

    presenter.EditableInputGap.prototype.connectEvents = function () {
        this.$view.on("input", this.onEdit.bind(this));
        this.$view.on("blur", this.blurHandler.bind(this));
        this.$view.on('keyup', this.onKeyUp.bind(this));
        this.$view.on("keypress", this.onKeyPress.bind(this));
        this.$view.off('change').bind('change', this.onEdit.bind(this));
    };

    presenter.EditableInputGap.prototype.createView = function () {
        var $gapView;
        if (presenter.hasMathGaps()) {
            $gapView = $(presenter.$view).find("input[id='"+this.objectID+"']");
        } else {
            var inputType = "text";
            if (presenter.configuration.useNumericKeyboard) {
                inputType = "tel";
            }
            $gapView = $('&lt;input type="' + inputType + '" value="" id="' + this.objectID + '" /&gt;');
            $gapView.css({
                width: presenter.configuration.gapWidth + "px"
            });

            $gapView.addClass("ic_gap");
            if (presenter.configuration.useNumericKeyboard) {
                $gapView.attr("step", "any");
            }
        }
        $gapView.attr("autocomplete", "off");
        return $gapView;
    };

    presenter.EditableInputGap.prototype.onKeyUp = function(event) {
        event.stopPropagation();
        if (presenter.configuration.useNumericKeyboard) {
            var newText = String(event.target.value);
            var pattern = StringUtils.getNumericPattern();
            if (newText.length &gt; 0 &amp;&amp; !newText.match(pattern)) {
                var patternWithoutLastCharacter = pattern.slice(0, -1);
                var regExp = RegExp(patternWithoutLastCharacter);
                var match = regExp.exec(newText);

                if (match) {
                    this.setViewValue(match[0]);
                } else {
                    this.setViewValue("");
                }
                this.value = this.getViewValue();
            }
        }
    };

    presenter.EditableInputGap.prototype.onKeyPress = function(event) {
        event.stopPropagation();
        if (presenter.configuration.useNumericKeyboard) {
            var key = String.fromCharCode(!event.charCode ? event.which : event.charCode);
            var selectionStartIdx = event.target.selectionStart;
            var selectionEndIdx = event.target.selectionEnd;
            var oldText = String(event.target.value);
            var newText = oldText.substring(0, selectionStartIdx)
                            + key
                            + oldText.substring(selectionEndIdx);
            var pattern = StringUtils.getNumericPattern();
            if (!newText.match(pattern)) {
                event.preventDefault();
            }
        }
    };

    presenter.EditableInputGap.prototype.onEdit = function (event) {
        this.notifyEdit();
        this.value = this.getViewValue();
    };

    presenter.EditableInputGap.prototype.blurHandler = function (event) {
        this.notify();
    };

    presenter.EditableInputGap.prototype.getViewValue = function () {
        return this.$view.val();
    };

    presenter.EditableInputGap.prototype.lock = function () {
        this.isDisabled = true;
        this.$view.attr('disabled','disabled');
    };

    presenter.EditableInputGap.prototype.unlock = function () {
        this.isDisabled = false;
        this.$view.removeAttr('disabled');
    };

    presenter.EditableInputGap.prototype.getValue = function () {
        return this.value;
    };

    presenter.EditableInputGap.prototype.setViewValue = function (value) {
        return this.$view.val(value);
    };

    presenter.filterSelectedItem = function(selectedItem) {
        return selectedItem.type !== 'image';
    }

    presenter.DraggableDroppableGap = function (htmlID, correctAnswers, gapScore) {
        var configuration = {
            addonID: presenter.configuration.addonID,
            objectID: htmlID,
            eventBus: presenter.eventBus,
            getSelectedItem: presenter.getSelectedItem,
            createView: presenter.DraggableDroppableGap.prototype.createView,

            showAnswersValue: correctAnswers,
            fillGap: presenter.DraggableDroppableGap.prototype.fillGap,
            makeGapEmpty: presenter.DraggableDroppableGap.prototype.makeGapEmpty,
            filterSelectedItem: presenter.filterSelectedItem,

            gapScore: gapScore,
            maxLength: presenter.getMaxLength(correctAnswers)
        };

        presenter.GapUtils.call(this, configuration);
        this.setGapWidth();
    };

    presenter.DraggableDroppableGap.prototype = Object.create(presenter.GapUtils.prototype);
    presenter.DraggableDroppableGap.parent = presenter.GapUtils.prototype;
    presenter.DraggableDroppableGap.constructor = presenter.DraggableDroppableGap;

    presenter.DraggableDroppableGap.prototype.createView = function () {
        var $view = DraggableDroppableObject.prototype.createView.call(this);

        $view.css({
            width: presenter.configuration.gapWidth + "px",
            display: 'inline-block'
        });

        $view.addClass("draggable-gap");
        $view.addClass("ic_gap");

        return $view;
    };

    presenter.DraggableDroppableGap.prototype.lock = function () {
        this.isDisabled = true;
        DraggableDroppableObject.prototype.lock.call(this);
    };

    presenter.DraggableDroppableGap.prototype.unlock = function () {
        this.isDisabled = false;
        DraggableDroppableObject.prototype.unlock.call(this);
    };

    presenter.DraggableDroppableGap.prototype.fillGap = function (selectedItem) {
        DraggableDroppableObject.prototype.fillGap.call(this, selectedItem);
        if (this.$view) {
            this.$view.html(presenter.textParser.parseAltTexts(this.$view.html()));
        }
        this.addCssClass("gapFilled");
        this.notify();

        presenter.renderMathJax();
        presenter.rerenderMathJax();
    };

    presenter.DraggableDroppableGap.prototype.makeGapEmpty = function () {
        DraggableDroppableObject.prototype.makeGapEmpty.call(this);
        this.removeCssClass("gapFilled");
        this.notify();
    };

    presenter.getMaxLength = function (correctAnswers) {
        let maxLength = presenter.configuration.gapMaxLength.value;

        correctAnswers.forEach(answers =&gt; {
            if (answers.length &gt; maxLength) {
                maxLength = answers.length;
            }
        });

        return maxLength;
    };

    presenter.GapsContainerObject = function () {
        this.gaps = [];
    };

    presenter.GapsContainerObject.prototype.addGap = function (gap) {
        this.gaps.push(gap);
    };

    presenter.GapsContainerObject.prototype.replaceDOMViewWithGap = function () {
        this.gaps.filter(function (gap) {
            return gap.gapType === presenter.GapUtils.GAP_TYPE.NORMAL;
        }).forEach(function (gap) {
            var gapID = "#" + gap.getObjectID(),
                element = presenter.$view.find(gapID),
                placeholder = element.attr("placeholder");

            element.replaceWith(gap.getView());
            element = presenter.$view.find(gapID);
            if(placeholder) {
                element.attr("placeholder", placeholder);
                element.removeClass("ic_gap").addClass("ic_filled_gap");
            }
        });
    };

    presenter.GapsContainerObject.prototype.updateGapMaxLength = function () {
        this.gaps.filter(function (gap) {
            return gap.gapType === presenter.GapUtils.GAP_TYPE.NORMAL;
        }).forEach(gap =&gt; {
            const gapMaxLength = gap.getGapMaxLength();
            const element = presenter.$view.find(`#${gap.getObjectID()}`);
            const placeholder = element.attr('placeholder');
            const placeholderLength = placeholder ? placeholder.length : 0;

            gap.setGapMaxLength(Math.max(gapMaxLength, placeholderLength));
        });
    };

    presenter.GapsContainerObject.prototype.addMaxLengthParamToGaps = function () {
        this.gaps.filter(gap =&gt; {
            return gap.gapType === presenter.GapUtils.GAP_TYPE.NORMAL;
        }).forEach(gap =&gt; {
            const gapMaxLength = gap.getGapMaxLength();
            const element = presenter.$view.find(`#${gap.getObjectID()}`);
            $(element).attr('maxlength', gapMaxLength);
        });
    };

    presenter.GapsContainerObject.prototype.replaceGapsDOMWithView = function () {
        this.gaps.forEach(function (gap) {
            gap.$view = presenter.$view.find("#" + gap.getObjectID());
            if(!presenter.isPreview){
                gap.connectEvents();
            }
        });
    };

    presenter.GapsContainerObject.prototype.removeAllGapsClasses = function () {
        this.gaps.forEach(function (gap) {
            gap.removeAllClasses();
        });
    };

    presenter.GapsContainerObject.prototype.check = function (isSetShow) {
        this.gaps.forEach(function (gap) {
            gap.check(isSetShow);
        });
    };

    presenter.GapsContainerObject.prototype.showAnswers = function () {
        this.gaps.forEach(function (gap) {
            gap.showAnswers();
        });
    };

    presenter.GapsContainerObject.prototype.showAnswer = function (item) {
        this.gaps[item].showAnswers();
    }

    presenter.GapsContainerObject.prototype.showAnswersMath = function () {
        this.gaps.forEach(function (gap) {
            if(gap.mathShowAnswersValue !== ""){
                gap.showAnswers();
            }else{
                gap.lock();
            }
        });
    };

    presenter.GapsContainerObject.prototype.hideAnswers = function () {
        this.gaps.forEach(function (gap) {
            gap.hideAnswers();
            gap.removeAllClasses();
        });
    };

    presenter.GapsContainerObject.prototype.reset = function () {
        this.gaps.forEach(function (gap) {
            gap.reset();
        });
    };

    presenter.GapsContainerObject.prototype.getErrorCount = function () {
        return this.gaps.reduce(function (errorCount, gap) {
            return errorCount += gap.getErrorCount();
        }, 0);
    };

    presenter.GapsContainerObject.prototype.getScore = function () {
        return this.gaps.reduce(function (score, gap) {
            return score += gap.getScore();
        }, 0);
    };

    presenter.GapsContainerObject.prototype.getMaxScore = function () {
        return this.gaps.reduce(function (maxScore, gap) {
            return maxScore += gap.gapScore;
        }, 0);
    };

    presenter.GapsContainerObject.prototype.markGapByIndexWithClass = function (index, cssClass) {
        this.gaps[index].removeAllClasses();
        this.gaps[index].addCssClass(cssClass);
        this.gaps[index].notifyEdit();
    };

    presenter.GapsContainerObject.prototype.markGapByIndexAsCorrect = function (index) {
        this.markGapByIndexWithClass(index, this.gaps[index].correctCSS);
    };

    presenter.GapsContainerObject.prototype.markGapByIndexAsWrong = function (index) {
        this.markGapByIndexWithClass(index, this.gaps[index].wrongCSS);
    };

    presenter.GapsContainerObject.prototype.markGapByIndexAsEmpty = function (index) {
        this.markGapByIndexWithClass(index, 'ic_gap-empty');
    };

    presenter.GapsContainerObject.prototype.getLength = function () {
        return this.gaps.length;
    };

    presenter.GapsContainerObject.prototype.getGapValueByIndex = function (index) {
        return this.gaps[index].getValue();
    };

    presenter.GapsContainerObject.prototype.setGapTextByIndex = function (index, text) {
        this.gaps[index].setValue(text);
        this.gaps[index].setViewValue(text);
        this.gaps[index].notifyEdit();
    };

    presenter.GapsContainerObject.prototype.setLockGapByIndex = function (index, value) {
        this.gaps[index].setIsLocked(value);
    };

    presenter.GapsContainerObject.prototype.lockGapByIndex = function (index) {
        this.setLockGapByIndex(index, true);
    };

    presenter.GapsContainerObject.prototype.unlockGapByIndex = function (index) {
        this.setLockGapByIndex(index, false);
    };

    presenter.GapsContainerObject.prototype.unlockAllGaps = function () {
        this.gaps.map(function (gap, index) {
            this.unlockGapByIndex(index);
        }, this);
    };

    presenter.GapsContainerObject.prototype.lockAllGaps = function () {
        this.gaps.map(function (gap, index) {
            this.lockGapByIndex(index);
        }, this);
    };

    presenter.GapsContainerObject.prototype.setEnableGapByIndex = function (index, isEnabled) {
        this.gaps[index].setIsEnabled(isEnabled);
        this.gaps[index].notifyEdit();
    };

    presenter.GapsContainerObject.prototype.disableGapByIndex = function (index) {
        this.setEnableGapByIndex(index, false);
    };

    presenter.GapsContainerObject.prototype.enableGapByIndex = function (index) {
        this.setEnableGapByIndex(index, true);
    };

    presenter.GapsContainerObject.prototype.enableAllGaps = function () {
        this.gaps.map(function (gap, index) {
            this.enableGapByIndex(index);
        }, this);
    };

    presenter.GapsContainerObject.prototype.disableAllGaps = function () {
        this.gaps.map(function (gap, index) {
            this.disableGapByIndex(index);
        }, this);
    };

    presenter.GapsContainerObject.prototype.getGapIndexByObjectID = function (objectID) {
        for (var index = 0; index &lt; this.gaps.length; index++) {
            if (this.gaps[index].getObjectID() === objectID) {
                return (index + 1);
            }
        }
    };

    presenter.GapsContainerObject.prototype.getGapsState = function () {
        return this.gaps.map(function (gap) {
            return gap.getGapState();
        });
    };

    presenter.GapsContainerObject.prototype.getState = function () {
        return this.gaps.map(function (gap) {
            return gap.getState();
        });
    };

    presenter.GapsContainerObject.prototype.setGapsState = function (state) {
        state.map(function (stateData, index) {
            var configuration = {
                droppedElement: undefined,
                isAttempted: stateData.isAttempted === undefined ? false : stateData.isAttempted,
                isEnabled: stateData.isEnabled,
                source: "",
                value: stateData.value
            };

            this.gaps[index].setState(configuration);
            this.gaps[index].$view.trigger('change');
        }, this);
    };

    presenter.GapsContainerObject.prototype.setSpansState = function (state, undefinedAttr) {
        if ((state !== undefinedAttr) &amp;&amp; (state !== null)) {
            state.map(function (stateData, index) {
                var configuration = {
                    value: stateData.value,
                    source: stateData.item,
                    isEnabled: undefined,
                    droppedElement: stateData.droppedElement
                };

                this.gaps[index].setState(configuration);

                if (stateData.value === "") {
                    this.gaps[index].destroyDraggableProperty();
                }else{
                    this.gaps[index].addCssClass("gapFilled");
                }

            }, this);
        }
    };

    presenter.GapsContainerObject.prototype.setMathShowAnswersValueByGapIndex = function (index, value) {
        this.gaps[index].setMathShowAnswersValue(value);
    };

    presenter.GapsContainerObject.prototype.isGapAttempted = function (index) {
        return this.gaps[index].isAttempted;
    };

    presenter.GapsContainerObject.prototype.isAnyGapAttempted = function () {
        return this.gaps.some(function (gap) {
            return gap.isAttempted;
        });
    };

    presenter.GapsContainerObject.prototype.setIsAttemptedByGapId = function(objectID, value) {
        for (var index = 0; index &lt; this.gaps.length; index++) {
            if (this.gaps[index].getObjectID() === objectID) {
                this.gaps[index].setAttempted(value);
                break;
            }
        }
    };


    presenter.ValueChangeObserver = function () {};

    presenter.ValueChangeObserver.prototype.notify = function (data) {
        presenter.eventBus.sendEvent('ValueChanged', this.getEventData(data));

        if (presenter.isAllOK()) presenter.sendAllOKEvent();
    };

    presenter.ValueChangeObserver.prototype.getEventData = function (data) {
        return {
            score: data.isCorrect ? "1" : "0",
            value: data.value,
            source: presenter.configuration.addonID,
            item: presenter.gapsContainer.getGapIndexByObjectID(data.objectID)
        };
    };

    presenter.isActivity = function () {
        return presenter.configuration.isActivity;
    };

    presenter.setMathShowAnswersCounter = function (counter) {
        presenter.configuration.mathShowAnswersCounter = counter;
    };

    presenter.tickMathCounter = function () {
        if (presenter.configuration.mathShowAnswersCounter) {
            presenter.configuration.mathShowAnswersCounter = presenter.configuration.mathShowAnswersCounter - 1;
        }
    };

    presenter.shouldTriggerMathShowAnswers = function () {
        return presenter.configuration.mathShowAnswersCounter === 0;
    };

    //showAnswers from Math
    presenter.setGapAnswer = function (gapIndex, answer, counter) {
        if (presenter.configuration.mathShowAnswersCounter === undefined) {
            presenter.setMathShowAnswersCounter(counter);
        }

        presenter.gapsContainer.setMathShowAnswersValueByGapIndex((gapIndex - 1), answer);

        presenter.tickMathCounter();

        if (presenter.shouldTriggerMathShowAnswers()) {
            presenter.gapsContainer.removeAllGapsClasses();
            presenter.gapsContainer.showAnswersMath();
            presenter.setMathShowAnswersCounter(counter);
        }
    };

    //hideAnswers from Math
    //hideAnswers is called anyway so just void function
    presenter.setUserValue = function () {};

    presenter.markConnectionWithMath = function() {
        isConnectedWithMath = true;
        presenter.isConnectedWithMath = true;
    };

    presenter.getValue = function (index) {
        return presenter.gapsContainer.getGapValueByIndex((index-1))
    };

    presenter.renderMathJax = function () {
        const args = [];
        args.push("Typeset", MathJax.Hub, presenter.$view.find(".table-addon-wrapper")[0]);
        MathJax.Hub.Queue(args);

        addDisplayStyleToMathJaxElements();
    };

    presenter.rerenderMathJax = function () {
        const args = [];
        args.push("Rerender", MathJax.Hub, presenter.$view.find(".table-addon-wrapper")[0]);
        MathJax.Hub.Queue(args);

        addDisplayStyleToMathJaxElements();
    };

    function addDisplayStyleToMathJaxElements() {
        $('annotation-xml').css({display: 'flex'});
    }

    function TableKeyboardController (elements, columnsCount, rowsCount) {
        var newElements = accountForMergedCells(elements, columnsCount, rowsCount);
        KeyboardController.call(this, newElements, columnsCount);
        this.rowsCount = rowsCount;
    }

    function accountForMergedCells(elements, columnsCount, rowsCount) {
        if (columnsCount === undefined || rowsCount === undefined ||
            elements.length === 0 || elements[0].getAttribute === undefined) return elements;
        var elementsArray = Array.from(Array(rowsCount), () =&gt; new Array(columnsCount));
        var i = 0;
        for (var ri = 0; ri &lt; rowsCount; ri++) {
            for (var ci = 0; ci &lt; columnsCount; ci++) {
                if (elementsArray[ri][ci] === undefined &amp;&amp; i &lt; elements.length) {
                    var element = elements[i];
                    i++;
                    var colNumber = getColspan(element);
                    var rowNumber = getRowspan(element);
                    for (var rj = 0; rj &lt; rowNumber; rj++) {
                        for (var cj = 0; cj &lt; colNumber; cj++) {
                            var newColIndex = ci + cj;
                            var newRowIndex = ri + rj;
                            if (newRowIndex &lt; rowsCount &amp;&amp; newColIndex &lt; columnsCount) {
                                elementsArray[newRowIndex][newColIndex] = element;
                            }
                        }
                    }
                }
            }
        }
        var newElements = [];
        for (var i = 0; i &lt; rowsCount; i++) newElements = newElements.concat(elementsArray[i]);
        return newElements;
    }

    function getColspan(element) {
        return getAttributeNumberValue(element, "colspan", 1);
    }

    function getRowspan(element) {
        return getAttributeNumberValue(element, "rowspan", 1);
    }

    function getAttributeNumberValue(element, attributeName, defaultValue) {
        if (element.getAttribute(attributeName)!= null &amp;&amp; !isNaN(element.getAttribute(attributeName))) {
            return new Number(element.getAttribute(attributeName));
        } else {
            return defaultValue;
        }
    }

    TableKeyboardController.prototype.reload = function (elements, columnsCount, rowsCount) {
        var newElements = accountForMergedCells(elements, columnsCount, rowsCount);
        KeyboardController.prototype.reload.call(this, newElements, columnsCount);
    }

    TableKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    TableKeyboardController.prototype.constructor = TableKeyboardController;

    TableKeyboardController.prototype.select = function (event) {
        event.preventDefault();
        presenter.addWhiteSpaceToValue();
        if (presenter.gapNavigation &amp;&amp; presenter.configuration.gapType === 'draggable' &amp;&amp; presenter.getCurrentGapsNumber() &gt; 0) {
            var $gap = presenter.getGap(presenter.gapIndex);

            if (!$gap || !$gap.is('span')) return;

            var oldVal = $gap.text();
            $gap.click();
            var value = $gap.text();

            if (0 !== oldVal.localeCompare(value)) {
                var data = [];
                if (value) {
                    data.push(window.TTSUtils.getTextVoiceObject(value, presenter.configuration.langTag));
                    data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.inserted));
                } else {
                    data.push(window.TTSUtils.getTextVoiceObject(oldVal, presenter.configuration.langTag));
                    data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.removed));
                }
                presenter.speak(data);
            }
        }
    };

    presenter.addWhiteSpaceToValue = function () {
        if (!presenter.getGap(presenter.gapIndex)) return;

        var gap = presenter.getGap(presenter.gapIndex)[0];
        var classNames = ['ic_filled_gap', 'ic_gap'];
        var isInputTypeGap = classNames.some(className =&gt; gap.classList.contains(className));
        if (!gap || !isInputTypeGap) return;

        var oldValue = gap.value;
        $(gap).val(`${oldValue} `);
    }

    TableKeyboardController.prototype.mark =  function (element) {
        KeyboardController.prototype.mark.call(this, element);
        this.getTarget(element, false).focus();
    };

    TableKeyboardController.prototype.unmark = function (element) {
        KeyboardController.prototype.unmark.call(this, element);
        this.getTarget(element, false).blur();
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject = new TableKeyboardController(presenter.getElementsForKeyboardNavigation(), presenter.configuration.columnsCount, presenter.configuration.rowsCount);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find('.table_cell');
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    TableKeyboardController.prototype.getTarget = function (element, willBeClicked){
        return $(element);
    };

    TableKeyboardController.prototype.escape = function (event) {
        if (presenter.gapNavigation) {
            if (event) {
                event.stopPropagation();
                event.preventDefault();
            }
            presenter.gapNavigation = false;
            presenter.clearCurrentCell();
            presenter.readCurrentCellTitle();
        } else {
            // must be set to false, otherwise module won't exit navigation
            presenter.addonKeyboardNavigationActive = false;
            KeyboardController.prototype.escape.call(this, event);
        }
    };

    TableKeyboardController.prototype.enter = function (event){
        KeyboardController.prototype.enter.call(this, event);

        if (presenter.addonKeyboardNavigationActive &amp;&amp; !presenter.gapNavigation) {
            presenter.gapNavigation = true;
            presenter.selectGap(0);
        }
        if (!presenter.addonKeyboardNavigationActive){
            presenter.addonKeyboardNavigationActive = true;
            presenter.readCurrentCellTitle();
        } else {
            presenter.readCurrentNavigationElement();
        }
    }

    KeyboardController.prototype.switchElement = function (move) {
        var new_position_index = this.keyboardNavigationCurrentElementIndex + move;
        if (new_position_index &gt;= this.keyboardNavigationElementsLen) {
            new_position_index = new_position_index - this.keyboardNavigationElementsLen;
        } else if (new_position_index &lt; 0) {
            new_position_index = this.keyboardNavigationElementsLen + new_position_index;
        }
        if (this.keyboardNavigationCurrentElement === this.keyboardNavigationElements[new_position_index]
            &amp;&amp; this.keyboardNavigationCurrentElementIndex != new_position_index) {
                this.keyboardNavigationCurrentElementIndex = new_position_index;
                if (move == this.columnsCount || move == -1 * this.columnsCount) {
                    if (new_position_index + move &gt;=0 &amp;&amp; new_position_index + move &lt; this.keyboardNavigationElementsLen) {
                        this.switchElement(move);
                    } else {
                        this.markCurrentElement(new_position_index);
                    }
                } else {
                if (move &lt; 0) {
                    if (new_position_index % this.columnsCount != 0) {
                        this.switchElement(-1);
                    } else {
                        this.markCurrentElement(new_position_index);
                    }
                } else {
                    if (new_position_index % this.columnsCount != this.columnsCount - 1) {
                        this.switchElement(1);
                    } else {
                        this.markCurrentElement(new_position_index);
                    }
                }
            }
        } else {
            this.markCurrentElement(new_position_index);
        }
    };

    presenter.readCurrentNavigationElement = function() {
        var html = $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement);
        var data = window.TTSUtils.getTextVoiceArrayFromElementWithGaps(html, presenter.configuration.langTag, presenter.speechTexts);
        presenter.speak(data);
    };

    presenter.readCurrentCellTitle = function() {
        var row = Math.floor(presenter.keyboardControllerObject.keyboardNavigationCurrentElementIndex / presenter.configuration.columnsCount);
        var column = presenter.keyboardControllerObject.keyboardNavigationCurrentElementIndex % presenter.configuration.columnsCount;
        var element = presenter.keyboardControllerObject.keyboardNavigationCurrentElement;
        var rowSpan = getRowspan(element);
        var colSpan = getColspan(element);

        if (rowSpan != 1 || colSpan != 1) {
            var classNames = element.className.split(' ');
            for (var i = 0; i &lt; classNames.length; i++) {
                var className = classNames[i];
                if (className.startsWith('col_')) {
                    var numberString = className.replace('col_', '').trim();
                    if (numberString.length &gt; 0 &amp;&amp; !isNaN(numberString)) column = new Number(numberString) - 1;
                }
                if (className.startsWith('row_')) {
                    var numberString = className.replace('row_', '').trim();
                    if (numberString.length &gt; 0 &amp;&amp; !isNaN(numberString)) row = new Number(numberString) - 1;
                }
            }
        }
        var alphabet = "ABCDEFGHIJKLMNOPRSTUWXYZ";
        var content = presenter.speechTexts.cell + " " + alphabet[column % alphabet.length] + " " + (row+1);
        var data = [window.TTSUtils.getTextVoiceObject(content)];
        if (rowSpan &gt; 1) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.rowSpan+" "+rowSpan));
        }
        if (colSpan &gt; 1) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.colSpan+" "+colSpan));
        }
        if (presenter.configuration.isFirstRowHeader) {
            data = data.concat(getCellHeaderUtterances('.row_1.col_'+(column+1), presenter.speechTexts.column));
        }
        if (presenter.configuration.isFirstColumnHeader) {
            data = data.concat(getCellHeaderUtterances('.col_1.row_'+(row+1), presenter.speechTexts.row));
        }
        presenter.speak(data);
    };

    function getCellHeaderUtterances(headerSelector, headerTypeSpeechText) {
        var data = [];
        var html = presenter.$view.find(headerSelector);
        if (html.length &gt; 0) {
            var headerContent = window.TTSUtils.getTextVoiceArrayFromElementWithGaps(html, presenter.configuration.langTag, presenter.speechTexts);
            if (headerContent.length &gt; 0) {
                data.push(window.TTSUtils.getTextVoiceObject(headerTypeSpeechText));
                data = data.concat(headerContent);
            }
        }
        return data;
    }

    // TAB or Right Arrow
    TableKeyboardController.prototype.nextElement = function (event) {
        if(event.keyCode === 9) { //TAB
            this.handleTab(event);
            event.preventDefault();
            return;
        }

        if (presenter.gapNavigation){
            presenter.switchSelectedValue(1);
            return;
        }

        event.preventDefault();

        if (this.keyboardNavigationCurrentElementIndex % this.columnsCount === this.columnsCount - 1) {
            presenter.readCurrentCellTitle();
        } else {
            this.switchElement(1);
            presenter.readCurrentCellTitle();
        }
    };

    // SHIFT+TAB or Left Arrow
    TableKeyboardController.prototype.previousElement = function (event) {
        if(event.keyCode === 9) { //TAB
            this.handleTab(event);
            return;
        }

        if (presenter.gapNavigation){
            presenter.switchSelectedValue(-1);
            return;
        }

        event.preventDefault();

        if (this.keyboardNavigationCurrentElementIndex % this.columnsCount === 0) {
            presenter.readCurrentCellTitle();
        } else {
            this.switchElement(-1);
            presenter.readCurrentCellTitle();
        }
    };

    TableKeyboardController.prototype.handleTab = function (event) {
        if (presenter.getCurrentGapsNumber() &gt; 0) {
            if (!presenter.gapNavigation) {
                presenter.gapNavigation = true;
                presenter.selectGap(0);
            } else {
                if ( !event.shiftKey ) {
                    presenter.selectGap(presenter.gapIndex + 1);
                } else {
                    presenter.selectGap(presenter.gapIndex - 1);
                }
            }

            var $gap = presenter.getGap(presenter.gapIndex);
            var $cell = $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement);
            var data = window.TTSUtils.getTextVoiceArrayFromGap($gap, $cell, presenter.configuration.langTag, presenter.speechTexts);
            presenter.speak(data);

        }

    };

    // UP Arrow
    TableKeyboardController.prototype.previousRow = function (event) {
        if (presenter.gapNavigation) {
            presenter.switchSelectedValue(-1);
            return;
        }

        if (event) {
            event.preventDefault();
        }

        if (Math.floor(this.keyboardNavigationCurrentElementIndex / this.columnsCount) === 0) {
            presenter.readCurrentCellTitle();
        } else {
            this.switchElement(-this.columnsCount);
            presenter.readCurrentCellTitle();
        }
    };

    // DOWN Arrow
    TableKeyboardController.prototype.nextRow = function (event) {
        if (presenter.gapNavigation) {
            presenter.switchSelectedValue(1);
            return;
        }

        if (event) {
            event.preventDefault();
        }

        if (Math.floor(this.keyboardNavigationCurrentElementIndex / this.columnsCount) === this.rowsCount - 1) {
            presenter.readCurrentCellTitle();
        } else {
            this.switchElement(this.columnsCount);
            presenter.readCurrentCellTitle();
        }
    };

    function getTableKeyboardController() {
        return TableKeyboardController;
    }

    getTableKeyboardController().prototype.exitWCAGMode = function () {
        presenter.gapNavigation = false;
        presenter.clearCurrentCell();
        presenter.addonKeyboardNavigationActive = false;
        KeyboardController.prototype.exitWCAGMode.call(this);
    };

    presenter.getTextToSpeechOrNull = function AddonTable_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function AddonTable_setWCAGStatus(isOn) {
        presenter.isWCAGOn = isOn;
    };

    presenter.speak = function AddonTable_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.isDeactivationBlocked = function() {
        return presenter.addonKeyboardNavigationActive || presenter.gapNavigation;
    };

    presenter.getCurrentGapsNumber = function() {
        return $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement).find('.ic_gap, ic_inlineChoice, .ic_filled_gap').length;
    };

    presenter.clearCurrentCell = function() {
        var $cell = $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement);
        $cell.find('.keyboard_navigation_active_element').removeClass('keyboard_navigation_active_element');
        if ($cell.find('input:focus').length &gt; 0) {
            $cell.find('input:focus').blur();
        }
    };

    presenter.getGap = function (index) {
        var $gaps = $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement).find('.ic_gap, ic_inlineChoice, .ic_filled_gap');
        if ($gaps.length === 0) return;
        if (index &lt; 0) index = 0;
        if (index &gt;= $gaps.length) index = $gaps.length-1;
        return $gaps.eq(index);
    };

    presenter.selectGap = function(index) {
        var $gaps = $(presenter.keyboardControllerObject.keyboardNavigationCurrentElement).find('.ic_gap, ic_inlineChoice, .ic_filled_gap');
        if ($gaps.length === 0) return;
        if(index &lt; 0) index = 0;
        if(index &gt;= $gaps.length) index = $gaps.length - 1;
        var $gap = $gaps.eq(index);

        presenter.clearCurrentCell();
        $gap.addClass('keyboard_navigation_active_element');

        presenter.gapIndex = index;
        if ($gap.is('input')) {
            $gap.focus();
        }
    };

    presenter.switchSelectedValue = function(move) {
        if(presenter.isShowAnswersActive || presenter.isSetShowErrorsMode) return;
        var $gap = presenter.getGap(presenter.gapIndex);

        if (!$gap || !$gap.is('select')) return;

        var index = $gap.prop("selectedIndex");
        var optionSize = $gap.find('option').size();
        index = index + move;

        if (0 &lt;= index &amp;&amp; index &lt; optionSize) {
            $gap.prop("selectedIndex", index);
            $gap.change();
            var value = $gap.val();
            if (value.length === 0 || value === '-' || value === '---') {
                var data = [window.TTSUtils.getTextVoiceObject(presenter.speechTexts.empty)];
            } else {
                var data = [window.TTSUtils.getTextVoiceObject($gap.val(), presenter.configuration.langTag)];
            }
            presenter.speak(data);
        }
    };

    /**
    * @method isAttempted method returning if user has interacted with addon
    * @return boolean
    */
    presenter.isAttempted = function AddonTable_isAttempted () {
        if (presenter.configuration.isNotActivity) {
            return true;
        }

        return presenter.gapsContainer.getLength() === 0 ? true : presenter.gapsContainer.isAnyGapAttempted();
    };


    /**
     * @param gapIndex - index of gap to check
     * @returns boolean
     */
    presenter.isGapAttempted = function AddonTableisGapAttempted (gapIndex) {
        if (gapIndex &gt; 0) {
            gapIndex = gapIndex - 1;
        }

        return presenter.gapsContainer.isGapAttempted(gapIndex);
    };

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

    function isPrintableShowAnswersStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
    }
    function isPrintableShowUserAnswersStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
    }
    function isPrintableCheckAnswersStateMode() {
        return presenter.printableStateMode === presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
    }
    function isPrintableStateMode() {
        return isPrintableShowUserAnswersStateMode() || isPrintableCheckAnswersStateMode();
    }

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.printableController = null;

    presenter.setPrintableController = function(controller) {
        presenter.textParser = new TextParserProxy(controller.getTextParser());
        presenter.printableController = controller;
    }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        } else {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    presenter.setPrintableAsyncCallback = function(id, callback) {
        presenter.printableParserID = id;
        presenter.printableParserCallback = callback;
    }

    presenter.isPrintableAsync = function() {
        return true;
    }

    presenter.hasMathGaps = function() {
        return presenter.configuration.gapType === 'math';
    }

    presenter.getPrintableHTML = function(model, showAnswers) {
        presenter.configuration = presenter.validateModel(presenter.upgradeModel(model));
        chosePrintableStateMode(showAnswers);
        createPrintableHTMLStructure(model);

        const parsingGapsResult = parsePrintableGaps(presenter.$view[0].innerHTML);
        presenter.$view.html(parsingGapsResult.html);
        presenter.$view.addClass("printable_module");

        const clone = presenter.$view.clone();
        clone.attr('id', presenter.printableParserID);

        parsePrintableHTMLUsingMathJax(parsingGapsResult);
        return clone[0].outerHTML;
    };

    function createPrintableHTMLStructure(model) {
        createPrintableHTMLWrapper(model);
        var $table = presenter.generateTable(presenter.configuration.contents, false);
        presenter.$wrapper.append($table);
    }

    function createPrintableHTMLWrapper(model) {
        presenter.$view = $('&lt;div&gt;&lt;/div&gt;');
        presenter.$view.attr("id", model.ID);
        presenter.$view.addClass('printable_addon_Table');
        presenter.$view.css('max-width', model.Width + 'px');
        presenter.$wrapper = $('&lt;div&gt;&lt;/div&gt;');
        presenter.$wrapper.addClass('table-addon-wrapper');
        presenter.$view.append(presenter.$wrapper);

        presenter.$view.addClass(getViewClassesBasedOnMode());
    }

    function getViewClassesBasedOnMode() {
        switch(presenter.printableStateMode) {
            case presenter.PRINTABLE_STATE_MODE.EMPTY: {
                return "printable_addon_Table-empty-mode";
            }
            case presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS: {
                return "printable_addon_Table-show-answers";
            }
            case presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS: {
                return "printable_addon_Table-show-user-answers";
            }
            case presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS: {
                return "printable_addon_Table-check-answers";
            }
        }

    }

    function matchGap(gapRegex, textToSearch, closingSignGapSize) {
        let closingBracketIndex = 0;

        let gapMatch = textToSearch.match(gapRegex);
        if (gapMatch == null)
            return [];

        const gapsMatches = [];
        while (gapMatch != null) {
            const textWithoutFoundGapBeginning = textToSearch.substring(gapMatch.index + gapMatch[0].length);

            closingBracketIndex = presenter.textParser.findClosingBracket(textWithoutFoundGapBeginning);
            if (closingBracketIndex &gt; -1) {
                const gapEndIndex = gapMatch.index + gapMatch[0].length + closingBracketIndex + closingSignGapSize;
                const wholeGapTextToTheEndingBracket = textToSearch.substring(gapMatch.index, gapEndIndex);
                // ex: \gap{lorem|ispum|\(\frac{1}{4}\)}
                gapsMatches.push(wholeGapTextToTheEndingBracket);
                textToSearch = textToSearch.substring(gapEndIndex);
            } else {
                // gap has no ending bracket, so syntax is broken and in fact it doesn't contain gap, ex:
                // \gap{lorem|ispum|\(\frac{1}{4}\)
                break;
            }

            gapMatch = textToSearch.match(gapRegex);
        }

        return gapsMatches;
    }

    function matchGapAndFilledGap(html) {
        const gapsRegex = /\\gap{|\\filledGap{/;
        return matchGap(gapsRegex, html, "}".length);
    }

    function matchDropdownGap(html) {
        // match last {{ ex. \frac{1}{{{gap_definition}}} will match last two "{{" from "{{{"
        const gapRegex = /{{(?!{)/;
        return matchGap(gapRegex, html, "}}".length);
    }

    function parsePrintableEditabeAndDropdownGaps (html) {
        let gapsMatches = matchGapAndFilledGap(html);
        gapsMatches = gapsMatches.concat(matchDropdownGap(html));

        const tablePrintableOptions = indexRegexMatchesBaseOnGapsTypes(gapsMatches);
        for (let i = 0; i &lt; tablePrintableOptions.length; i++) {
            const gapScriptID = presenter.configuration.addonID + '.' + (i+1);
            const tablePrintableOption = tablePrintableOptions[i];
            if (presenter.printableController) {
                if (presenter.printableController.hasCalculatedGapCorrect(gapScriptID)) {
                    tablePrintableOption.setCalculatedCorrect(presenter.printableController.getCalculatedGapCorrect(gapScriptID));
                }
                tablePrintableOption.setCalculatedAnswer(presenter.printableController.getCalculatedGapAnswer(gapScriptID));
            }
            const optionHTML = tablePrintableOption.getPrintableHTML();
            html = html.replace(tablePrintableOption.text, optionHTML);
        }
        return {
            printableOptions: tablePrintableOptions,
            html: html
        };
    }

    /**
     Parse printable html using MathJax

     To properly parse math gaps, gaps need to be transformed to special syntax \gap{id|width|size|{{value:value}}
     and it needs to be passed into mathjax processor.
     Then gaps will be changed to html inputs which can be transformed into proper printable gap html

     In addition to the gaps, the HTML must also be processed. Therefore, even though the gaps are not of the Math type,
     their content may still require this processing, so it is necessary to wait for the HTML to be generated.
     */
    function parsePrintableHTMLUsingMathJax(parsingGapsResult){
        function mathJaxTypesetEnd() {
            if (presenter.hasMathGaps()) {
                // replace each input with its corresponding gap html
                presenter.$view.find('input').each(function (index, element) {
                    const html = parsingGapsResult.printableOptions[index].getPrintableHTML();
                    $(this).replaceWith(html);
                });
            }

            presenter.notifyParserCallback(presenter.$view[0].outerHTML);
        }

        const args = [];
        args.push("Typeset", MathJax.Hub, presenter.$view[0]);
        args.push(mathJaxTypesetEnd);
        MathJax.Hub.Queue(args);
    }

    function parsePrintableGaps(html) {
        if (presenter.hasMathGaps()) {
            return parsePrintableMathGaps(html);
        }
        return parsePrintableEditabeAndDropdownGaps(html);
    }

    function parsePrintableMathGaps(html) {
        const gapsMatches = matchGapAndFilledGap(html);
        const tablePrintableOptions = indexRegexMatchesBaseOnGapsTypes(gapsMatches);

        for (var i = 0; i &lt; tablePrintableOptions.length; i++) {
            if (presenter.printableController) {
                const tablePrintableOption = tablePrintableOptions[i];
                const gapScriptID = presenter.configuration.addonID + '.' + (i+1);
                if (presenter.printableController.hasCalculatedGapCorrect(gapScriptID)) {
                    tablePrintableOption.setCalculatedCorrect(presenter.printableController.getCalculatedGapCorrect(gapScriptID));
                }
                tablePrintableOption.setCalculatedAnswer(presenter.printableController.getCalculatedGapAnswer(gapScriptID));
            }
        }

        // replace normal gap syntax with math gap syntax
        for (let i = 0; i &lt; gapsMatches.length; i++) {
            const match = gapsMatches[i];
            const gapId = `${presenter.configuration.addonID}-${i}`;
            html = html.replace(match, generateMathGap(gapId, presenter.configuration.gapWidth.value));
        }

        return {
            printableOptions: tablePrintableOptions,
            html: html
        };
    }

    presenter.notifyParserCallback = function (outerHTML) {
        presenter.printableStateMode = null;

        presenter.printableParserCallback(
            presenter.textParser.parseAltTexts(outerHTML)
        );
    };

    function indexRegexMatchesBaseOnGapsTypes(gapsMatches) {
        const normalGapRegex = /\\gap{.*?}/g,
            filledGapRegex = /\\filledGap{.*?}/g,
            dropdownGapRegex = /{{.*?}}/g;
        var tablePrintableOptions = [],
            stateIndex = 0,
            isMatchesGapType,
            gapMatch,
            gapMatchIndex;

        for (gapMatchIndex = 0; gapMatchIndex &lt; gapsMatches.length; gapMatchIndex++) {
            gapMatch = gapsMatches[gapMatchIndex];

            isMatchesGapType = gapMatch.match(normalGapRegex);
            if (isMatchesGapType != null) {
                tablePrintableOptions.push(
                    new TablePrintableNormalGapOption(
                        gapMatch,
                        stateIndex
                    )
                )
                stateIndex += 1;
                continue;
            }

            isMatchesGapType = gapMatch.match(filledGapRegex);
            if (isMatchesGapType != null) {
                tablePrintableOptions.push(
                    new TablePrintableFilledGapOption(
                        gapMatch,
                        stateIndex
                    )
                )
                stateIndex += 1;
            }
        }
        for (gapMatchIndex = 0; gapMatchIndex &lt; gapsMatches.length; gapMatchIndex++) {
            gapMatch = gapsMatches[gapMatchIndex];

            isMatchesGapType = gapMatch.match(dropdownGapRegex);
            if (isMatchesGapType != null) {
                tablePrintableOptions.push(
                    new TablePrintableDropdownGapOption(
                        gapMatch,
                        stateIndex
                    )
                );
                stateIndex += 1;
            }
        }
        return tablePrintableOptions;
    }

    function getTextWidthInPixels(html) {
        var $outerLessonWrapper = $("&lt;div&gt;&lt;/div&gt;");
        $outerLessonWrapper.css("position", "absolute");
        $outerLessonWrapper.css("visibility", "hidden");
        $outerLessonWrapper.addClass("printable_lesson");

        var $outerPageWrapper = $("&lt;div&gt;&lt;/div&gt;");
        $outerPageWrapper.addClass("printable_page");
        $outerLessonWrapper.append($outerPageWrapper);

        var $outerModuleWrapper = $("&lt;div&gt;&lt;/div&gt;");
        $outerModuleWrapper.addClass("printable_module");
        $outerModuleWrapper.addClass("printable_addon_Table");
        $outerPageWrapper.append($outerModuleWrapper);

        var $wrapper = $("&lt;div&gt;&lt;/div&gt;");
		$wrapper.css("margin", "0px");
		$wrapper.css("padding", "0px");
		$wrapper.addClass("printable_gap");
		$outerModuleWrapper.append($wrapper);

		$wrapper.html(html);
		$("body").append($outerLessonWrapper);
		var width = $wrapper[0].getBoundingClientRect().width;
		$outerLessonWrapper.remove();
		return width;
    }

    presenter.gradualShowAnswers = function(item) {
        presenter.gapsContainer.showAnswer(item);
        presenter.renderMathJax();
    }

    presenter.gradualHideAnswers = function() {
         presenter.gapsContainer.hideAnswers();
         presenter.renderMathJax();
         presenter.isGradualShowAnswersActive = false;
    }

    presenter.getActivitiesCount = function () {
        return presenter.gapsContainer.getLength();
    }

    /**
    * TablePrintableOption
    * */
    function TablePrintableOption (text, stateID) {
        this.text = text;
        this.stateID = stateID;
        this.options = [];
    }

    TablePrintableOption.prototype = Object.create(Object.prototype);
    TablePrintableOption.prototype.constructor = TablePrintableOption;

    TablePrintableOption.prototype.getPrintableGapSignHTML = function() {
        var $signSpan = $("&lt;span&gt;&lt;/span&gt;");
        if (this.hasCorrectAnswer()) {
            $signSpan.addClass("printable_gap_correct");
        } else {
            $signSpan.addClass("printable_gap_wrong");
        }
        return $signSpan[0].outerHTML;
    }

    TablePrintableOption.prototype.setCalculatedAnswer = function(answer) {
        if (answer == null) return;
        this.calculatedAnswer = answer;
    }

    TablePrintableOption.prototype.setCalculatedCorrect = function(isCorrect) {
        if (isCorrect == null) return;
        this.calculatedCorrect = isCorrect;
    }

    /**
     * TablePrintableEditableGapOption
     */

    /**
     * @param text
     * @param stateID
     * @constructor
     */
    function TablePrintableEditableGapOption (text, stateID) {
        TablePrintableOption.call(this, text, stateID)
        this.initialValue = "";
        this.correctAnswer = "";
    }

    TablePrintableEditableGapOption.prototype = Object.create(TablePrintableOption.prototype);
    TablePrintableEditableGapOption.prototype.constructor = TablePrintableEditableGapOption;

    TablePrintableEditableGapOption.prototype.getPrintableHTML = function () {
        this.getGapTextData();
        var gapInnerText = this.generateInnerText();
        var gapHTML = this.generateGapHTML(gapInnerText);
        if (isPrintableCheckAnswersStateMode() &amp;&amp; this.hasAnswer()) {
            gapHTML += this.getPrintableGapSignHTML();
        }
        return gapHTML;
    }

    TablePrintableEditableGapOption.prototype.getAnswer = function () {
        const gapState = presenter.printableState.gaps[this.stateID];
        return gapState === undefined ? null : gapState.value;
    }

    TablePrintableEditableGapOption.prototype.hasCorrectAnswer = function () {
        if (this.calculatedCorrect !== undefined) return this.calculatedCorrect;
        return this.correctAnswer === this.getAnswer();
    }

    TablePrintableEditableGapOption.prototype.hasAnswer = function () {
        return this.getAnswer() !== "" &amp;&amp; this.getAnswer() !== null;
    }

    TablePrintableEditableGapOption.prototype.generateInnerText = function() {
        switch(presenter.printableStateMode) {
            case presenter.PRINTABLE_STATE_MODE.EMPTY: {
                return this.generateInnerTextForEmptyStateMode();
            }
            case presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS: {
                if (this.calculatedAnswer !== undefined) return this.calculatedAnswer;
                return this.correctAnswer;
            }
            case presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS:
            case presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS: {
                return this.hasAnswer() ? this.getAnswer() : this.generateInnerTextForEmptyStateMode();
            }
        }
    }

    TablePrintableEditableGapOption.prototype.generateInnerTextForEmptyStateMode = function(){
        var longestAnswer = "";
        for (var i = 0; i &lt; this.options.length; i++) {
            if (this.options[i].length &gt; longestAnswer.length) {
                longestAnswer = this.options[i];
            }
        }
        if (longestAnswer.length === 0)
            longestAnswer = "&amp;nbsp;&amp;nbsp;&amp;nbsp;";

        var gapWidth = 0;
        if (presenter.configuration.gapWidth.isSet) {
            gapWidth = presenter.configuration.gapWidth.value;
        } else {
            gapWidth = getTextWidthInPixels(longestAnswer);
        }

        var value = this.initialValue;
        var initialValueLength = 0;
        if (this.initialValue.length &gt; 0) {
            initialValueLength = getTextWidthInPixels(this.initialValue);
        }

        var spaceWidth = getTextWidthInPixels('&amp;nbsp;');
        var spaceCount = Math.ceil((gapWidth - initialValueLength) / spaceWidth);
        var maxSplitFreeWidth = 50; //must be at least minSplitSize * 2
        var minSplitSize = 20;

        if (spaceCount &gt; maxSplitFreeWidth) {
            for (i = 0; i &lt; minSplitSize; i++) {
                value += "&amp;nbsp;";
            }

            var nextNbsp = false;
            for (i = 0; i &lt; spaceCount - 2 * minSplitSize; i++) {
                if (nextNbsp) {
                    value += "&amp;nbsp;";
                } else {
                    value += " ";
                }
                nextNbsp = !nextNbsp;
            }

            for (i = 0; i &lt; minSplitSize; i++) {
                value += "&amp;nbsp;";
            }
        } else {
            for (i = 0; i &lt; spaceCount; i++) {
                value += "&amp;nbsp;";
            }
        }
        return value;
    }

    TablePrintableEditableGapOption.prototype.generateGapHTML = function(gapInnerText) {
        const $span = $("&lt;span&gt;&lt;/span&gt;");
        const classes = presenter.hasMathGaps() ? "printable_gap printable_math_gap" : "printable_gap";
        $span.addClass(classes);
        $span.html(gapInnerText);
        return $span[0].outerHTML;
    }

    /**
     * TablePrintableNormalGapOption
     */

    function TablePrintableNormalGapOption (text, stateID) {
        TablePrintableEditableGapOption.call(this, text, stateID)
    }

    TablePrintableNormalGapOption.prototype = Object.create(TablePrintableEditableGapOption.prototype);
    TablePrintableNormalGapOption.prototype.constructor = TablePrintableNormalGapOption;

    TablePrintableNormalGapOption.prototype.getGapTextData = function() {
        // remove "\\gap{" and last "}"
        this.options = this.text.replace("\\gap{", "").replace(/}$/, "").split("|");
        this.correctAnswer = this.options[0];
    }


    /**
     * TablePrintableFilledGapOption
     */

    function TablePrintableFilledGapOption (text, stateID) {
        TablePrintableEditableGapOption.call(this, text, stateID)
    }

    TablePrintableFilledGapOption.prototype = Object.create(TablePrintableEditableGapOption.prototype);
    TablePrintableFilledGapOption.prototype.constructor = TablePrintableFilledGapOption;

    TablePrintableFilledGapOption.prototype.getGapTextData = function() {
        this.options = this.text.replace("\\filledGap{", "").replace(/}$/, "").split("|");
        this.correctAnswer = this.options[1];
        this.initialValue = this.options.splice(0, 1)[0];
    }

    /**
    * TablePrintableDropdownGapOption
    * */

    function TablePrintableDropdownGapOption (text, stateID) {
        TablePrintableOption.call(this, text, stateID);
        this.correctOptionRegex = /[0-9]*?:/;
        this.correctOptionIndex = null;
        this.chosenOptionIndex = null;
    }

    TablePrintableDropdownGapOption.prototype = Object.create(TablePrintableOption.prototype);
    TablePrintableDropdownGapOption.prototype.constructor = TablePrintableDropdownGapOption;

    TablePrintableDropdownGapOption.prototype.getPrintableHTML = function() {
        this.getGapTextData();
        if (!presenter.configuration.keepOriginalOrder)
            this.sortOptions();
        this.findCorrectOptionIndex();
        this.removeScoreInformationFromCorrectOption();

        if (isPrintableStateMode())
            this.findChosenOptionIndex();

        var gapHTML = this.generateGapHTML();
        if (isPrintableCheckAnswersStateMode() &amp;&amp; this.hasAnswer()){
            gapHTML += this.getPrintableGapSignHTML();
        }
        return gapHTML;
    }

    TablePrintableDropdownGapOption.prototype.getAnswer = function () {
        return this.chosenOptionIndex;
    }

    TablePrintableDropdownGapOption.prototype.hasAnswer = function () {
        return this.chosenOptionIndex !== null;
    }

    TablePrintableDropdownGapOption.prototype.hasCorrectAnswer = function () {
        if (this.calculatedCorrect !== undefined) return this.calculatedCorrect;
        return this.chosenOptionIndex === this.correctOptionIndex;
    }

    TablePrintableDropdownGapOption.prototype.getGapTextData = function() {
        this.options = this.text.replace("{{","").replace("}}","").split("|");
    }

    TablePrintableDropdownGapOption.prototype.sortOptions = function() {
        this.options.sort(function(a,b){
            return a.replace(this.correctOptionRegex, "").localeCompare(b.replace(this.correctOptionRegex, ""));
        }.bind(this));
    }

    TablePrintableDropdownGapOption.prototype.findCorrectOptionIndex = function() {
        for (var i = 0; i &lt; this.options.length; i++) {
            if (this.correctOptionRegex.test(this.options[i])) {
                this.correctOptionIndex = i;
                return;
            }
        }
    }

    TablePrintableDropdownGapOption.prototype.removeScoreInformationFromCorrectOption = function() {
        this.options[this.correctOptionIndex]
            = this.options[this.correctOptionIndex].replace(this.correctOptionRegex, "");
    }

    TablePrintableDropdownGapOption.prototype.findChosenOptionIndex = function() {
        const gapState = presenter.printableState.gaps[this.stateID];
        const gapValue = gapState === undefined ? null : gapState.value;
        for (var i = 0; i &lt; this.options.length; i++) {
            if (this.options[i] === gapValue) {
                this.chosenOptionIndex = i;
                return;
            }
        }
    }

    TablePrintableDropdownGapOption.prototype.generateGapHTML = function() {
        var $span = $("&lt;span&gt;&lt;/span&gt;");
        $span.addClass("printable_gap");

        if (isPrintableShowAnswersStateMode()) {
            $span.html(this.options[this.correctOptionIndex]);
            this.options[this.correctOptionIndex] = $span[0].outerHTML;
        } else if (isPrintableStateMode()) {
            $span.html(this.options[this.chosenOptionIndex]);
            this.options[this.chosenOptionIndex] = $span[0].outerHTML;
        }
        return this.options.join(" / ");
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Table_Of_Contents" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="Header" nameLabel="Table_Of_Contents_property_header" type="string"/>
        <property displayName="Don't show pages" name="DontShowPages" nameLabel="Table_Of_Contents_property_dont_show_pages" type="string"/>
        <property displayName="Display Type" name="displayType" nameLabel="Table_Of_Contents_property_display_type" type="{default, list, comboList, icons, icons+list}"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Table_Of_Contents_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Table_Of_Contents_property_speech_texts" type="staticlist">
            <property name="Title" nameLabel="Table_Of_Contents_speech_text_title" type="staticrow">
                <property name="Title" nameLabel="Table_Of_Contents_speech_text_title" type="string"/>
            </property>
            <property name="GoToPage" nameLabel="Table_Of_Contents_speech_text_go_to_page" type="staticrow">
                <property name="GoToPage" nameLabel="Table_Of_Contents_speech_text_go_to_page" type="string"/>
            </property>
            <property name="GoToPageNumber" nameLabel="Table_Of_Contents_speech_text_go_to_page_number" type="staticrow">
                <property name="GoToPageNumber" nameLabel="Table_Of_Contents_speech_text_go_to_page_number" type="string"/>
            </property>
            <property name="PagesList" nameLabel="Table_Of_Contents_speech_text_pages_list" type="staticrow">
                <property name="PagesList" nameLabel="Table_Of_Contents_speech_text_pages_list" type="string"/>
            </property>
            <property name="Pagination" nameLabel="Table_Of_Contents_speech_text_pagination" type="staticrow">
                <property name="Pagination" nameLabel="Table_Of_Contents_speech_text_pagination" type="string"/>
            </property>
            <property name="OutOf" nameLabel="Table_Of_Contents_speech_text_out_of" type="staticrow">
                <property name="OutOf" nameLabel="Table_Of_Contents_speech_text_out_of" type="string"/>
            </property>
            <property name="Selected" nameLabel="Table_Of_Contents_speech_text_selected" type="staticrow">
                <property name="Selected" nameLabel="Table_Of_Contents_speech_text_selected" type="string"/>
            </property>
        </property>
	</model>
<css>.table-of-contents {
    border: 0;
    padding: 1px;
    color: #7C818E;
    direction: inherit;
}

.table-of-contents-title {
    height: 25px;
    font-weight: bold;
    color: #7F7F7F;
    font-family: Verdana, Tahoma, "DejaVu Sans", sans-serif;
    margin: 1px;
    display: table-cell;
    vertical-align: middle;
}

.table-of-contents-pagination {
    height: 30px;
    vertical-align: middle;
    text-decoration: none;
    position: absolute;
    bottom: 0;
}

.table-of-contents-pagination a,
.table-of-contents-pagination a:hover {
    text-decoration: none;
    margin-left: 5px;
    margin-right: 5px;
}

.table-of-contents-pagination .selected {
    text-decoration: underline;
}

.table-of-contents-list {
    overflow: hidden;
}

.table-of-contents-list ul {
    padding-left: 5px;
}

.table-of-contents-list ul li a {
    text-decoration: none;
}

.imageContainer {
    display: inline-block;
    margin: 5px;
    cursor: pointer;
}

.imageElement {
    width: 100px;
    height: 140px;
}

.iconsList {
    display: flex;
    width: 100%;
    overflow: auto;
}

.listElement {
    text-align: center;
    font-size: 15px;
    word-wrap: break-word;
    line-height: 17px;
    word-break: break-all;
}
</css><view>&lt;div class="table-of-contents"&gt;
    &lt;div class="table-of-contents-title"&gt;
        Table of Contents
    &lt;/div&gt;
    &lt;div class="table-of-contents-list"&gt;
        &lt;ol&gt;
        &lt;/ol&gt;
    &lt;/div&gt;
    &lt;div class="table-of-contents-pagination"&gt;
        &lt;a href="#"&gt;1&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="table-of-contents"&gt;
    &lt;div class="table-of-contents-title"&gt;
        Table of Contents
    &lt;/div&gt;
    &lt;div class="table-of-contents-list"&gt;
        &lt;ol&gt;
        &lt;/ol&gt;
    &lt;/div&gt;
    &lt;div class="table-of-contents-pagination"&gt;
        &lt;a href="#"&gt;1&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonTable_Of_Contents_create(){
    var presenter = function() {};
    var presentationController;

    var elementsHeights = {};

    presenter.isWCAGOn = false;
    presenter.keyboardControllerObject = null;
    presenter.isFirstEnter = true;

    presenter.ERROR_CODES = {
        E01: "Values in property 'Don't show' pages must be numeric",
        E02: "Values in property 'Don't show' pages must be greater than 0",
        E03: "Values in property 'Don't show' pages must be unique"
    };

    presenter.DISPAY_TYPES = {
        DEFAULT: "default",
        LIST: "list",
        COMBO_LIST: "comboList",
        ICONS: "icons",
        ICONS_LIST: "icons+list"
    }

    presenter.DEFAULT_TTS_PHRASES = {
        TITLE: "Title",
        GO_TO_PAGE: "Go to page",
        GO_TO_PAGE_NUMBER: "Go to page number",
        PAGES_LIST: "List of pages",
        PAGINATION: "Pagination",
        OUT_OFF: "out of",
        SELECTED: "Selected",
    };

    presenter.CSS_CLASSES = {
        TABLE_OF_CONTENTS: "table-of-contents",
        PAGINATION: "table-of-contents-pagination",
        LIST: "table-of-contents-list",
        TITLE: "table-of-contents-title",
        SELECTED: "selected",
        COMBO_LIST: "comboList",
        ICONS_LIST: "iconsList",
        IMAGE_CONTAINER: "imageContainer",
        IMAGE_ELEMENT: "imageElement",
        LIST_ELEMENT: "listElement",
        CURRENT_PAGE: "current-page",
    };

    presenter.ATTRIBUTES = {
        DATA_PAGE_NUMBER: "data-page-number",
    };

    function getErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    function getCorrectObject(v) { return { isValid: true, value: v }; }

    function setElementsDimensions(addonWidth, addonHeight) {
        var wrapper = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS}:first`)[0];
        var wrapperDimensions = DOMOperationsUtils.getOuterDimensions(wrapper);
        var wrapperDistances = DOMOperationsUtils.calculateOuterDistances(wrapperDimensions);
        $(wrapper).css({
            width: addonWidth - wrapperDistances.horizontal,
            height: addonHeight - wrapperDistances.vertical
        });

        elementsHeights.wrapper = $(wrapper).height();

        var title = presenter.$view.find(`.${presenter.CSS_CLASSES.TITLE}`)[0];
        var titleDimensions = DOMOperationsUtils.getOuterDimensions(title);
        var titleDistances = DOMOperationsUtils.calculateOuterDistances(titleDimensions);
        $(title).css({
            width: $(wrapper).width() - titleDistances.horizontal
        });

        elementsHeights.title = $(title).height() + titleDistances.vertical;

        var pagination = presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION}`)[0];
        var paginationDimensions = DOMOperationsUtils.getOuterDimensions(pagination);
        var paginationDistances = DOMOperationsUtils.calculateOuterDistances(paginationDimensions);
        $(pagination).css({
            width: $(wrapper).width() - paginationDistances.horizontal
        });

        elementsHeights.pagination = $(pagination).height() + paginationDistances.vertical;

        var list = presenter.$view.find(`.${presenter.CSS_CLASSES.LIST}`)[0];
        var listDimensions = DOMOperationsUtils.getOuterDimensions(list);
        var listDistances = DOMOperationsUtils.calculateOuterDistances(listDimensions);
        $(list).css({
            height: $(wrapper).height - elementsHeights.title - elementsHeights.pagination - listDistances.vertical,
            width: $(wrapper).width - listDistances.horizontal
        });

        elementsHeights.list = $(list).height() + listDistances.vertical;
    }


    function generateElement (text, page, isPreview) {
        var $element = $(document.createElement('li')),
            $link = $(document.createElement('a'));

        $link.text(text);
        $link.attr('href', '#');
        $element.html($link);
        
        if (!isPreview){
            var presentation = presentationController.getPresentation(),
                currentPageIndex = presentation.getPage(presentationController.getCurrentPageIndex()).getId();
            
            if (currentPageIndex == page.index) {
                $link.addClass(presenter.CSS_CLASSES.CURRENT_PAGE);
            }
        }
        
        return $element;
    }

    function generateListElements (isPreview) {
        var $list = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.LIST} ol`);

        for (var i = 0; i &lt; presenter.pages.length; i++) {
            $list.append(generateElement(presenter.pages[i].name, presenter.pages[i], isPreview));
        }

        return $list.outerHeight();
    }

    function generateComboElement (text, isPreview) {
        var element;

        if(!isPreview){
            var presentation = presentationController.getPresentation();
            var currentPageName = presentation.getPage(presentationController.getCurrentPageIndex()).getName();

            if(text == currentPageName){
                element = $('&lt;option selected&gt;&lt;/option&gt;');
            }else{
                element = $('&lt;option&gt;&lt;/option&gt;');
            }
        }else{
            element = $('&lt;option&gt;&lt;/option&gt;');
        }
        element.text(text);

        return element;
    }

    function generateComboList (isPreview) {
        var selectionList = $('&lt;select class="comboList"&gt;&lt;/select&gt;');
        presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS}`).append(selectionList);
        var comboList = presenter.$view.find(`.${presenter.CSS_CLASSES.COMBO_LIST}`);
        $(comboList).css("width", "100%");

        for (var i = 0; i &lt; presenter.pages.length; i++) {
            comboList.append(generateComboElement(presenter.pages[i].name, isPreview));
        }
    }

    function generateIconListElement (page, isPreview) {
        var anchorElement = $('&lt;a&gt;&lt;/a&gt;');
        $(anchorElement).addClass(presenter.CSS_CLASSES.IMAGE_CONTAINER);

        var imgElement = document.createElement('img');
        $(imgElement).addClass(presenter.CSS_CLASSES.IMAGE_ELEMENT);
        imgElement.src = page.preview;

        var listElement = $('&lt;div&gt;&lt;/div&gt;');
        $(listElement).addClass(presenter.CSS_CLASSES.LIST_ELEMENT);
        $(listElement).text(page.name);
        $(anchorElement).append(imgElement).append(listElement);

        if(!isPreview) {
            var presentation = presentationController.getPresentation(),
                commander = presentationController.getCommands(),
                currentPageIndex = presentation.getPage(presentationController.getCurrentPageIndex()).getId();

            $(anchorElement).attr(presenter.ATTRIBUTES.DATA_PAGE_NUMBER, page.numberOfIndex + 1);

            if (currentPageIndex == page.index) {
                $(anchorElement).addClass(presenter.CSS_CLASSES.CURRENT_PAGE);
            }

            $(anchorElement).click(function (event) {
                event.stopPropagation();
                event.preventDefault();

                if (currentPageIndex !== page.index) {
                    commander.gotoPageIndex(page.numberOfIndex);
                }
            });
        }

        return anchorElement;
    }

    function generateIconElement (page, isPreview) {
        var anchorElement = $('&lt;a class="imageContainer"&gt;&lt;/a&gt;');
        $(anchorElement).addClass(presenter.CSS_CLASSES.IMAGE_CONTAINER);
        var imgElement = document.createElement('img');

        $(imgElement).addClass(presenter.CSS_CLASSES.IMAGE_ELEMENT);
        imgElement.src = page.preview;
        $(anchorElement).append(imgElement);

        if(!isPreview) {
            var presentation = presentationController.getPresentation(),
                commander = presentationController.getCommands(),
                currentPageIndex = presentation.getPage(presentationController.getCurrentPageIndex()).getId();

            $(anchorElement).attr(presenter.ATTRIBUTES.DATA_PAGE_NUMBER, page.numberOfIndex + 1);

            if (currentPageIndex == page.index) {
                $(anchorElement).addClass(presenter.CSS_CLASSES.CURRENT_PAGE);
            }

            $(anchorElement).click(function (event) {
                event.stopPropagation();
                event.preventDefault();

                if (currentPageIndex !== page.index) {
                    commander.gotoPageIndex(page.numberOfIndex);
                }
            });
        }

        return anchorElement;
    }

    function generateIcons (isPreview) {
        var iconsList = $('&lt;div&gt;&lt;/div&gt;');
        iconsList.addClass(presenter.CSS_CLASSES.ICONS_LIST);
        presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS}`).append(iconsList);

        for (var i = 0; i &lt; presenter.pages.length; i++) {
            iconsList.append(generateIconElement(presenter.pages[i], isPreview));
        }
    }

    function generateIconsAndList (isPreview) {
        var iconsList = $('&lt;div&gt;&lt;/div&gt;');
        iconsList.addClass(presenter.CSS_CLASSES.ICONS_LIST);
        presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS}`).append(iconsList);

        for (var i = 0; i &lt; presenter.pages.length; i++) {
            iconsList.append(generateIconListElement(presenter.pages[i], isPreview));
        }
    }

    presenter.pageStartIndex = function(page) {
        var index = 0;
        for (var i = 0; i &lt; page; i++) {
            index += presenter.pagination.pages[i].length;
        }
        return index+1;
    };

    function displayPage(page) {
        var $list = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.LIST} ol`),
            pages = presenter.pagination.pages[page], i,
            startIndex = presenter.pageStartIndex(page),
            $pageList = presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION}`);

        $list.find('li').hide();
        $list.attr('start', '' + startIndex);
        for (i = 0; i &lt; pages.length; i++) {
            $list.find('li:eq(' + pages[i] + ')').show();
        }

        $pageList.children().removeClass(presenter.CSS_CLASSES.SELECTED);
        $pageList.find('a').each(function(){
            if ($(this).text()==(page+1)) {
                $(this).addClass(presenter.CSS_CLASSES.SELECTED);
            }
        });
   }

    function isSpaceSufficient($list, spareHeight) {
        return $list.find('li:first').outerHeight() &lt; spareHeight;
    }

    function paginateList(spareHeight, isPreview) {
        var $list = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.LIST} ol`);
        var $pagination = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.PAGINATION}`);

        if (!isSpaceSufficient($list, spareHeight)) return false;

        var currentPageHeight = 0, page, i;
        presenter.pagination = {
            pages: [[]],
            size: 0
        };

        $list.find('li').each(function (index, value) {
            var outerHeight = $(value).outerHeight();

            if (currentPageHeight + outerHeight &gt; spareHeight) {
                presenter.pagination.size++;
                currentPageHeight = 0;
                presenter.pagination.pages[presenter.pagination.size] = [];
            }

            currentPageHeight += outerHeight;
            presenter.pagination.pages[presenter.pagination.size].push(index);
        });

        for (i = 1; i &lt;= presenter.pagination.size; i++) {
            var $element = $(document.createElement('a'));
            $element.text(i + 1);
            $element.attr('href', '#');
            $pagination.append($element);
        }

        displayPage(0);
        if (!isPreview) handlePaginationMouseActions();

        return true;
    }

    function handleMouseClickActions() {
        var commander = presentationController.getCommands(),
            presentation = presentationController.getPresentation(),
            $list = presenter.$view.find(`.${presenter.CSS_CLASSES.LIST} ol`),
            currentPageIndex = presentation.getPage(presentationController.getCurrentPageIndex()).getId(),
            pageName;

        if(presenter.isComboListDisplayType()){
            presenter.$view.find(`.${presenter.CSS_CLASSES.COMBO_LIST}`).change(function(event){
                event.stopPropagation();
                event.preventDefault();
                pageName = $(this).val();
                for(var p in presenter.pages) {
                    var page = presenter.pages[p];

                    if (currentPageIndex !== page.index &amp;&amp; pageName === page.name) {
                        commander.gotoPageIndex(page.numberOfIndex);
                    }
                }
            });
        }else{
            $list.find('li a').each(function () {
                $(this).click(function (event) {
                    event.stopPropagation();
                    event.preventDefault();
                    pageName = $(this).text();
                    for(var p in presenter.pages) {
                        var page = presenter.pages[p];

                        if (currentPageIndex !== page.index &amp;&amp; pageName === page.name) {
                            commander.gotoPageIndex(page.numberOfIndex);
                        }
                    }
                });
            });
        }
    }

    function handlePaginationMouseActions() {
        var lists = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.LIST}`),
        $pagination = presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION}`);

        $pagination.click(function (event) {
            event.stopPropagation();
        });

        presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION} a`).each(function() {
            $(this).click(function(event) {
                event.stopPropagation();
                event.preventDefault();
                displayPage(parseInt($(this).text(), 10) - 1);
            });
        });
    }

    function presenterLogic(view, model, isPreview) {
        var upgradedModel = presenter.upgradeModel(model);
        presenter.setSpeechTexts(upgradedModel["speechTexts"]);
        presenter.configuration = presenter.validateModel(upgradedModel);

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        function reportInsufficientSpace() {
            presenter.$view.html('&lt;strong&gt;Available space is insufficient! Please enlarge addon dimensions.&lt;/strong&gt;')
        }

        presenter.pages = isPreview ? mockPresentationPages() : presenter.getPresentationPages();
        presenter.$view = $(view);

        setElementsDimensions(upgradedModel.Width, upgradedModel.Height);

        if(presenter.isComboListDisplayType()){
            generateComboList(isPreview);
        }else if(presenter.isIconsDisplayType()){
            generateIcons(isPreview);
        }else if(presenter.isIconsListDisplayType()){
            generateIconsAndList(isPreview);
        }else{
            var listHeight = generateListElements(isPreview),
                spareHeight = elementsHeights.wrapper - elementsHeights.title;

            var $list = presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.LIST} ol`);
            if (!isSpaceSufficient($list, spareHeight)) {
                reportInsufficientSpace();
            }
        }

        if ((listHeight &gt; spareHeight) &amp;&amp; (presenter.isDefaultDisplayType())) {
            if (!paginateList(spareHeight - elementsHeights.pagination, isPreview)) {
                reportInsufficientSpace();
            }
        } else {
            presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION}`).hide();
        }

        if(presenter.isListDisplayType()){
            var titleHeight = presenter.$view.find(`.${presenter.CSS_CLASSES.TITLE}`).height();
            presenter.$view.find(`.${presenter.CSS_CLASSES.LIST}`).css({
               "height":  upgradedModel.Height-titleHeight+"px",
               "overflow-y": "scroll"
            });
        }

        if (!isPreview) handleMouseClickActions();
        if (!ModelValidationUtils.isStringEmpty(upgradedModel['Header'])) {
        	presenter.$view.find(`.${presenter.CSS_CLASSES.TABLE_OF_CONTENTS} .${presenter.CSS_CLASSES.TITLE}`).text(upgradedModel['Header'])
        }

         presenter.buildKeyboardController();
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeLangTag(model);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.upgradeLangTag = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] =  '';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Title"]) {
            upgradedModel["speechTexts"]["Title"] = {Title: ""};
        }
        if (!upgradedModel["speechTexts"]["GoToPage"]) {
            upgradedModel["speechTexts"]["GoToPage"] = {GoToPage: ""};
        }
        if (!upgradedModel["speechTexts"]["GoToPageNumber"]) {
            upgradedModel["speechTexts"]["GoToPageNumber"] = {GoToPageNumber: ""};
        }
        if (!upgradedModel["speechTexts"]["PagesList"]) {
            upgradedModel["speechTexts"]["PagesList"] = {PagesList: ""};
        }
        if (!upgradedModel["speechTexts"]["Pagination"]) {
            upgradedModel["speechTexts"]["Pagination"] = {Pagination: ""};
        }
        if (!upgradedModel["speechTexts"]["OutOf"]) {
            upgradedModel["speechTexts"]["OutOf"] = {OutOf: ""};
        }
        if (!upgradedModel["speechTexts"]["Selected"]) {
            upgradedModel["speechTexts"]["Selected"] = {Selected: ""};
        }

        return upgradedModel;
    };

    presenter.validateHiddenPages = function(hiddenPages) {
        if (typeof(hiddenPages) == 'undefined') {
            hiddenPages = '';
        }

        var pages = hiddenPages.split(';').sort();

        for (var i = 0; i &lt; pages.length; i++) {
            var numberObject = ModelValidationUtils.validateInteger(pages[i]);

            if (!numberObject.isValid &amp;&amp; hiddenPages.length &gt; 0) {
                return getErrorObject("E01");
            }

            if (pages[i] &lt; 0) {
                return getErrorObject("E02");
            }

            if (pages[i] === pages[i - 1]) {
                return getErrorObject("E03");
            }
        }
        return getCorrectObject(pages);
    }

    presenter.validateModel = function(model) {
        var pagesValidationResult = presenter.validateHiddenPages(model['DontShowPages']);
        if (!pagesValidationResult.isValid) {
            return pagesValidationResult;
        }

        return {
            ID: model.ID,
            isValid: true,
            hiddenPages: pagesValidationResult.value,
            displayType: model.displayType,
            langTag: model.langAttribute,
        };
    };

    presenter.getPresentationPages = function() {
        var pages = [],
            presentation = presentationController.getPresentation(),
            pageCount = presentation.getPageCount();

        for (var i = 0; i &lt; pageCount; i++) {
            if ($.inArray(String(i+1), presenter.configuration.hiddenPages) == -1) {
                var page = {};
                page.name = presentation.getPage(i).getName();
                page.index = presentation.getPage(i).getId();
                page.numberOfIndex = i;
                page.preview = presentation.getPage(i).getPreview();

                pages.push(page);
            }
        }

        return pages;
    };

    function mockPresentationPages() {
        return [
            {index:"fwrg4g1",
             name:"Page 01",
             numberOfIndex:"0"},
            {index:"fwrg4g2",
             name:"Page 02",
             numberOfIndex:"1"},
            {index:"fwrg4g3",
             name:"Page 03",
             numberOfIndex:"2"},
            {index:"fwrg4g4",
             name:"Page 04",
             numberOfIndex:"3"},
            {index:"fwrg4g5",
             name:"Page 05",
             numberOfIndex:"4"},
            {index:"fwrg4g6",
             name:"Page 06",
             numberOfIndex:"5"},
            {index:"fwrg4g7",
             name:"Page 07",
             numberOfIndex:"6"},
            {index:"fwrg4g8",
             name:"Page 08",
             numberOfIndex:"7"},
            {index:"fwrg4g9",
             name:"Page 09",
             numberOfIndex:"8"},
            {index:"fwrg4g0",
             name:"Page 10",
             numberOfIndex:"9"}
        ];
    }

    presenter.createPreview = function(view, model){
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);
    };

    presenter.setPlayerController = function(controller) {
        presentationController = controller;
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject = new TocKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
        presenter.keyboardControllerObject.selectEnabled(true);
    };

    presenter.getElementsForTTS = function() {
        var elements = presenter.getElementsForKeyboardNavigation();
        elements.unshift(presenter.getTitleElementForKeyboardNavigation());
        return elements;
    };

    presenter.getElementsForKeyboardNavigation = function() {
        if (presenter.isComboListDisplayType()){
            return presenter.getComboListDisplayTypeElementsForKeyboardNavigation();
        } else if (presenter.isIconsDisplayType() || presenter.isIconsListDisplayType()) {
            return presenter.getIconsDisplayTypeElementsForKeyboardNavigation();
        } else if (presenter.isDefaultDisplayType()) {
            return presenter.getDefaultDisplayTypeElementsForKeyboardNavigation();
        } else if (presenter.isListDisplayType()) {
            return presenter.getListDisplayTypeElementsForKeyboardNavigation();
        }
    };

    presenter.getDefaultDisplayTypeElementsForKeyboardNavigation = function() {
        var elements = presenter.getListDisplayTypeElementsForKeyboardNavigation();

        for (var i = 0; i &lt; presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION} a`).length; i++) {
            elements.push($(presenter.$view.find(`.${presenter.CSS_CLASSES.PAGINATION} a`)[i]));
        }

        return elements;
    };

    presenter.getListDisplayTypeElementsForKeyboardNavigation = function() {
        var elements = [];

        for (var i = 0; i &lt; presenter.$view.find('li a').length; i++) {
            elements.push($(presenter.$view.find('li a')[i]));
        }

        return elements;
    };

    presenter.getIconsDisplayTypeElementsForKeyboardNavigation = function() {
        var elements = [];

        for (var i = 0; i &lt; presenter.$view.find(`.${presenter.CSS_CLASSES.ICONS_LIST} a`).length; i++) {
            elements.push($(presenter.$view.find(`.${presenter.CSS_CLASSES.ICONS_LIST} a`)[i]));
        }

        return elements;
    };

    presenter.getComboListDisplayTypeElementsForKeyboardNavigation = function() {
        var elements = [];

        for (var i = 0; i &lt; presenter.$view.find('select').length; i++) {
            elements.push($(presenter.$view.find('select')[i]));
        }

        return elements;
    };

    presenter.getTitleElementForKeyboardNavigation = function() {
        return $(presenter.$view.find(`.${presenter.CSS_CLASSES.TITLE}`)[0]);
    };

    presenter.isDefaultDisplayType = function() {
        var displayType = presenter.configuration.displayType;
        return (
          displayType == presenter.DISPAY_TYPES.DEFAULT
          || displayType == ""
          || displayType == undefined
        )
    };

    presenter.isListDisplayType = function() {
        return presenter.configuration.displayType == presenter.DISPAY_TYPES.LIST;
    };

    presenter.isComboListDisplayType = function() {
        return presenter.configuration.displayType == presenter.DISPAY_TYPES.COMBO_LIST;
    };

    presenter.isIconsDisplayType = function() {
        return presenter.configuration.displayType == presenter.DISPAY_TYPES.ICONS;
    };

    presenter.isIconsListDisplayType = function() {
        return presenter.configuration.displayType == presenter.DISPAY_TYPES.ICONS_LIST;
    };

    presenter.keyboardController = function(keycode, isShiftDown, event) {
        if (presenter.isComboListDisplayType()) {
            presenter.$view.find('select').focus();
        }
        presenter.keyboardControllerObject.handle(keycode, isShiftDown, event);
    };

    function TocKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    TocKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    TocKeyboardController.prototype.constructor = TocKeyboardController;

    function scrollHorizontally(element) {
        var pos = $(element).position().left,
            currentscroll = presenter.$view.find(`.${presenter.CSS_CLASSES.ICONS_LIST}`).scrollLeft(),
            divwidth = presenter.$view.find(`.${presenter.CSS_CLASSES.ICONS_LIST}`).width();

        pos=(pos+currentscroll)-(divwidth/2);

        presenter.$view.find(`.${presenter.CSS_CLASSES.ICONS_LIST}`).scrollLeft(pos);
    }

    function scrollVertically(element) {
        var pos = $(element).position().top,
            currentscroll = presenter.$view.find(`.${presenter.CSS_CLASSES.LIST}`).scrollTop(),
            divheight = presenter.$view.find(`.${presenter.CSS_CLASSES.LIST}`).height();

        pos=(pos+currentscroll)-(divheight/2);

        presenter.$view.find(`.${presenter.CSS_CLASSES.LIST}`).scrollTop(pos);
    }

    function centerElement(element){
        if(presenter.isIconsDisplayType() || presenter.isIconsListDisplayType()){
            scrollHorizontally(element);
        }else {
            scrollVertically(element);
        }
    }

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            Title: presenter.DEFAULT_TTS_PHRASES.TITLE,
            GoToPage: presenter.DEFAULT_TTS_PHRASES.GO_TO_PAGE,
            GoToPageNumber: presenter.DEFAULT_TTS_PHRASES.GO_TO_PAGE_NUMBER,
            PagesList: presenter.DEFAULT_TTS_PHRASES.PAGES_LIST,
            Pagination: presenter.DEFAULT_TTS_PHRASES.PAGINATION,
            OutOf: presenter.DEFAULT_TTS_PHRASES.OUT_OFF,
            Selected: presenter.DEFAULT_TTS_PHRASES.SELECTED,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Title: TTSUtils.getSpeechTextProperty(
                speechTexts.Title.Title,
                presenter.speechTexts.Title),
            GoToPage: TTSUtils.getSpeechTextProperty(
                speechTexts.GoToPage.GoToPage,
                presenter.speechTexts.GoToPage),
            GoToPageNumber: TTSUtils.getSpeechTextProperty(
                speechTexts.GoToPageNumber.GoToPageNumber,
                presenter.speechTexts.GoToPageNumber),
            PagesList: TTSUtils.getSpeechTextProperty(
                speechTexts.PagesList.PagesList,
                presenter.speechTexts.PagesList),
            Pagination: TTSUtils.getSpeechTextProperty(
                speechTexts.Pagination.Pagination,
                presenter.speechTexts.Pagination),
            OutOf: TTSUtils.getSpeechTextProperty(
                speechTexts.OutOf.OutOf,
                presenter.speechTexts.OutOf),
            Selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.Selected)
        };
    };

    // TAB or Right Arrow
    TocKeyboardController.prototype.nextElement = function (event) {
        if (event &amp;&amp; this.isActiveTTSOrCurrentElementNotComboList()) {
            event.preventDefault();
        }

        const isArrowRight = event &amp;&amp; event.keyCode === 39;
        if (isArrowRight &amp;&amp; this.isActiveTTSAndCurrentElementComboList()) {
            this.switchOption(1);
        } else {
            this.switchElement(1);

            if (this.isCurrentElementNotDisplayed()) {
                this.nextElement();
            }

            centerElement(this.keyboardNavigationCurrentElement);
        }
    };

    // SHIFT+TAB or Left Arrow
    TocKeyboardController.prototype.previousElement = function (event) {
        if (event &amp;&amp; this.isActiveTTSOrCurrentElementNotComboList()) {
            event.preventDefault();
        }

        const isArrowLeft = event &amp;&amp; event.keyCode === 37;
        if (isArrowLeft &amp;&amp; this.isActiveTTSAndCurrentElementComboList()) {
            this.switchOption(-1);
        } else {
            this.switchElement(-1);

            if(this.isCurrentElementNotDisplayed()) {
                this.previousElement();
            }

            centerElement(this.keyboardNavigationCurrentElement);
        }
    };

    // DOWN Arrow
    TocKeyboardController.prototype.nextRow = function (event) {
        if (event &amp;&amp; this.isActiveTTSOrCurrentElementNotComboList()) {
            event.preventDefault();
        }

        if (this.isActiveTTSAndCurrentElementComboList()) {
            this.switchOption(1);
        } else {
            this.switchElement(this.columnsCount);

            if(this.isCurrentElementNotDisplayed()) {
                this.nextRow();
            }

            centerElement(this.keyboardNavigationCurrentElement);
        }
    };

    // UP Arrow
    TocKeyboardController.prototype.previousRow = function (event) {
        if (event &amp;&amp; this.isActiveTTSOrCurrentElementNotComboList()) {
            event.preventDefault();
        }

        if (this.isActiveTTSAndCurrentElementComboList()) {
            this.switchOption(-1);
        } else {
            this.switchElement(-this.columnsCount);

            if (this.isCurrentElementNotDisplayed()) {
                this.previousRow();
            }

            centerElement(this.keyboardNavigationCurrentElement);
        }
    };

    TocKeyboardController.prototype.switchElement = function (move) {
        KeyboardController.prototype.switchElement.call(this, move);
        if(!this.isCurrentElementNotDisplayed()) {
            this.readCurrentElement();
        }
    };

    TocKeyboardController.prototype.switchOption = function (move) {
        const $option = this.getCurrentElement();
        const optionSize = $option.find("option").size();
        const currentIndex = $option.prop("selectedIndex");
        const nextIndex = currentIndex + move;

        if (0 &lt;= nextIndex &amp;&amp; nextIndex &lt; optionSize) {
            $option.prop("selectedIndex", nextIndex);
            this.readCurrentElement();
        }
    };

    TocKeyboardController.prototype.select = function (event) {
        const $currentElement = this.getCurrentElement();
        const isCurrentElementComboList = isElementComboList($currentElement);
        const isActiveTTS = presenter.isTTS();

        if (event &amp;&amp; (isActiveTTS || !isCurrentElementComboList)) {
            event.preventDefault();
        }
        if (!this.isSelectEnabled) {
            return;
        }

        if (isActiveTTS &amp;&amp; isCurrentElementComboList) {
            $currentElement.change();
        } else {
            this.selectAction();
            if (isParentOfElementPaginationList($currentElement)) {
                presenter.speak(presenter.speechTexts.Selected);
            }
        }
    };

    TocKeyboardController.prototype.isCurrentElementNotDisplayed = function () {
        return this.getCurrentElement().parent().style("display") === "none";
    };

    TocKeyboardController.prototype.isActiveTTSAndCurrentElementComboList = function () {
        return presenter.isTTS() &amp;&amp; this.isCurrentElementComboList();
    };

    TocKeyboardController.prototype.isActiveTTSOrCurrentElementNotComboList = function () {
        return presenter.isTTS() || !this.isCurrentElementComboList();
    };

    TocKeyboardController.prototype.isCurrentElementComboList = function () {
        const $currentElement = this.getCurrentElement();
        return isElementComboList($currentElement);
    };

    TocKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    TocKeyboardController.prototype.enter = function (event) {
        if (presenter.isTTS() &amp;&amp; presenter.isFirstEnter) {
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForTTS());
            presenter.isFirstEnter = false;
        }
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

    TocKeyboardController.prototype.exitWCAGMode = function () {
        presenter.isFirstEnter = true;
        KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    TocKeyboardController.prototype.readCurrentElement = function () {
        var $element = this.getTarget(this.keyboardNavigationCurrentElement, false);

        if ($element.hasClass(presenter.CSS_CLASSES.TITLE)) {
            presenter.speak(getTitleTTS($element));
        } else {
            if ($element.parent().hasClass(presenter.CSS_CLASSES.PAGINATION)) {
                presenter.speak(getPaginationHyperLinkTTS($element));
            } else if (isElementComboList($element)) {
                presenter.speak(getComboListTTS($element));
            } else if (presenter.isIconsDisplayType()) {
                presenter.speak(getIconTTS($element));
            } else {
                presenter.speak(getHyperLinkTTS($element));
            }
        }
    };

    function isElementComboList($element) {
        return $element.hasClass(presenter.CSS_CLASSES.COMBO_LIST);
    }

    function isParentOfElementPaginationList($element) {
        return $element.parent().hasClass(presenter.CSS_CLASSES.PAGINATION);
    }

    function getTitleTTS($element) {
        var textVoiceObject = [];

        const titlePrefix = presenter.speechTexts.Title;
        pushMessageToTextVoiceObject(textVoiceObject, titlePrefix);

        const titleText = $element[0].innerText;
        pushMessageToTextVoiceObject(textVoiceObject, titleText, true);

        return textVoiceObject;
    }

    function getHyperLinkTTS($element) {
        const goToPagePrefix = presenter.speechTexts.GoToPage;
        const pageName = $element[0].innerText;
        return `${goToPagePrefix} ${pageName}`;
    }

    function getPaginationHyperLinkTTS($element) {
        const language = document.documentElement.lang;
        const currentPaginationIndexAsText
          = window.TTSUtils.numberToOrdinalNumber($element[0].innerText, language, window.TTSUtils.GENDER.NEUTER);
        const paginationSizeAsText
          = window.TTSUtils.numberToOrdinalNumber(presenter.pagination.size + 1, language, window.TTSUtils.GENDER.FEMININE);

        var texts = [presenter.speechTexts.Pagination, ];
        if ($element.hasClass(presenter.CSS_CLASSES.SELECTED)) {
            texts.push(presenter.speechTexts.Selected);
        }
        texts.push(`${currentPaginationIndexAsText} ${presenter.speechTexts.OutOf} ${paginationSizeAsText}`);

        return createVoiceObject(texts);
    }

    function getIconTTS($element) {
        return `${presenter.speechTexts.GoToPageNumber} ${$element.attr(presenter.ATTRIBUTES.DATA_PAGE_NUMBER)}`;
    }

    function getComboListTTS($element) {
        var textVoiceObject = [];

        const prefix = presenter.speechTexts.PagesList;
        pushMessageToTextVoiceObject(textVoiceObject, prefix);

        const pageName = $element.val();
        const gotToPageText = `${presenter.speechTexts.GoToPage} ${pageName}`;
        pushMessageToTextVoiceObject(textVoiceObject, gotToPageText);

        return textVoiceObject;
    }

    presenter.getTextToSpeechOrNull = function Toc_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function(data) {
        var tts = presenter.getTextToSpeechOrNull(presentationController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.isTTS = function () {
        return presenter.getTextToSpeechOrNull(presentationController) &amp;&amp; presenter.isWCAGOn;
    };

    function createVoiceObject(messages, usePresenterLangTag = false) {
        var textVoiceObject = [];
        for (var i = 0; i &lt; messages.length; i++) {
            pushMessageToTextVoiceObject(textVoiceObject, messages[i], usePresenterLangTag);
        }
        return textVoiceObject;
    }

    function pushMessageToTextVoiceObject(textVoiceObject, message, usePresenterLangTag = false) {
        if (usePresenterLangTag)
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
        else
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="TextAudio" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Controls" name="controls" nameLabel="TextAudio_property_controls" type="{None, Browser, Custom}"/>
        <property displayName="Display time" name="displayTime" nameLabel="TextAudio_property_display_time" type="boolean"/>
        <property displayName="Enable loop" name="enableLoop" nameLabel="TextAudio_property_enable_loop" type="boolean"/>

        <property isDefault="true" name="Slides" nameLabel="TextAudio_property_slides" type="list">
            <property isLocalized="true" name="Text" nameLabel="TextAudio_property_text" type="html"/>
            <property isLocalized="true" name="Times" nameLabel="TextAudio_property_times" type="text"/>
            <property displayName="Position and Dimentions" isLocalized="true" name="positionAndDimentions" nameLabel="TextAudio_property_position_and_dimentions" type="text"/>
            <property displayName="File SRT" name="fileSRT" nameLabel="TextAudio_property_fileSRT" type="file"/>
            <property displayName="Crop time" name="cropTime" nameLabel="TextAudio_property_crop_time" type="string"/>
        </property>
        <property name="mp3" nameLabel="TextAudio_property_mp3" type="audio"/>
        <property name="ogg" nameLabel="TextAudio_property_ogg" type="audio"/>

        <property displayName="On Text Click Behavior" name="clickAction" nameLabel="TextAudio_property_click_action" type="{Play from the moment, Play the interval, Play the interval from vocabulary file}"/>
        <property displayName="Vocabulary mp3" name="vocabulary_mp3" nameLabel="TextAudio_property_vocabulary_mp3" type="audio"/>
        <property displayName="Vocabulary ogg" name="vocabulary_ogg" nameLabel="TextAudio_property_vocabulary_ogg" type="audio"/>
        <property displayName="Vocabulary intervals" isLocalized="true" name="vocabulary_intervals" nameLabel="TextAudio_property_vocabulary_intervals" type="text"/>

        <property displayName="On end" name="onEnd" nameLabel="TextAudio_property_vocabulary_on_end" type="event"/>
        <property displayName="Is Click Disabled" name="isClickDisabled" nameLabel="TextAudio_property_is_click_disabled" type="boolean"/>
        <property displayName="Show Slides" name="showSlides" nameLabel="TextAudio_property_show_slides" type="{Show current slide, Show all slides}"/>
        <property name="isDisabled" nameLabel="TextAudio_property_is_disabled" type="boolean"/>
        <property displayName="Enable playback speed controls" name="enablePlaybackSpeedControls" nameLabel="Enable_playback_speed_controls" type="boolean"/>
        <property name="langAttribute" nameLabel="TextAudio_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="TextAudio_property_speech_texts" type="staticlist">
            <property name="Play" nameLabel="TextAudio_play" type="staticrow">
                <property name="Play" nameLabel="TextAudio_play" type="string"/>
            </property>
            <property name="Pause" nameLabel="TextAudio_pause" type="staticrow">
                <property name="Pause" nameLabel="TextAudio_pause" type="string"/>
            </property>
            <property name="Stop" nameLabel="TextAudio_stop" type="staticrow">
                <property name="Stop" nameLabel="TextAudio_stop" type="string"/>
            </property>
            <property name="AudioSpeedController" nameLabel="TextAudio_audio_speed_controller" type="staticrow">
                <property name="AudioSpeedController" nameLabel="TextAudio_audio_speed_controller" type="string"/>
            </property>
            <property name="ProgressBar" nameLabel="TextAudio_progress_bar" type="staticrow">
                <property name="ProgressBar" nameLabel="TextAudio_progress_bar" type="string"/>
            </property>
            <property name="VolumeController" nameLabel="TextAudio_volume_controller" type="staticrow">
                <property name="VolumeController" nameLabel="TextAudio_volume_controller" type="string"/>
            </property>
        </property>
    </model>
<css>.wrapper-addon-textaudio {
    height: 30px;
    position: relative;
}

.wrapper-addon-textaudio audio {
    bottom: 0;
    position: absolute;
}

.wrapper-addon-textaudio .textaudio-text {
    width: 100%;
}

.wrapper-addon-textaudio  span {
    cursor: pointer;
}

.wrapper-addon-textaudio span.active {
    color: #cc0000;
}

.text-audio-loading-area {
    width: 100%;
    height: 100%;
    display: none;
    position: absolute;
    top: 0;
    left: 0;
    background: url("/media/images/loading.gif") no-repeat center;
    background-size: 30px;
}


.textaudioplayer {
    top: -40px;
    background-color: #EBEFF0;
    border-radius: 20px;
    width: 340px;
    height: 40px;
    display: table;
    padding-right: 2px;
    position: relative;
}

.textaudio-play-pause-btn {
    width: 38px;
    height: 40px;
    float:left;
    border: solid 1px #444;
    border-radius: 30px;
    background-position: center center;
    background-repeat: no-repeat;
    cursor: pointer;
}

.textaudio-play-btn {
    background-image: url('resources/audio-play-btn.png');
}

.textaudio-pause-btn {
    background-image: url('resources/audio-pause-btn.png');
}

.textaudio-stop-btn {
    width: 40px;
    height: 40px;
    background: url('resources/audio-stop-btn.png') no-repeat center center;
    float:left;
    cursor: pointer;
}

.textaudio-volume-btn {
    width: 40px;
    height: 40px;
    background: url('resources/audio-volume-btn.png') no-repeat center center;
    float:left;
    cursor: pointer;
}

.textaudio-volume0 {
    background-image: url('resources/volume-0.png');
}

.textaudio-volume1 {
    background-image: url('resources/volume-1.png');
}

.textaudio-volume2 {
    background-image: url('resources/volume-2.png');
}

.textaudio-volume3 {
    background-image: url('resources/volume-3.png');
}

.textaudio-player-time {
    vertical-align: middle;
    display: table-cell;
    height: 40px;
    line-height: 16px;
    padding-right: 10px;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
}

.textaudio-progress-bar {
    -ms-touch-action: none;
    touch-action: none;
    width: 100px;
    float:left;
    padding-left: 0px;
    margin-top: 15px;
    margin-left: 5px;
    height: 10px;
    background-color: #FFF;
    border-radius: 5px;
    position: relative;
}

.textaudio-slider-btn {
    width: 20px;
    height: 20px;
    background: url('resources/audio-slider-btn.png') no-repeat;
    float:left;
    margin-left: -10px;
    margin-top: -5px;
    position: absolute;
    cursor: pointer;
}

.textaudio-bar {
    background-color: #BDC2C7;
    float: left;
    height: 10px;
    margin-left: 0px;
    border-bottom-left-radius: 5px;
    border-top-left-radius: 5px;
}

.textaudio-volume-layer {
    width: 100px;
    height: 40px;
    position: absolute;
    right: 25px;
    cursor: pointer;
    background-color: inherit;
}

.textaudio-volume-control-background {
    position: absolute;
    width: 100%;
    height: 50%;
    border-bottom: solid 1px #808080;
}

.textaudio-volume-control {
    position: absolute;
    width: 20px;
    height: 40px;
    background: url('resources/audio-slider-btn.png') no-repeat center;
    margin-left: -10px;
}

.textaudioplayer.playback-speed-control-enabled {
    width: 460px;
}

.textaudio-playback-rate {
    float: left;
    vertical-align: middle;
    height: 30px;
    width: 60px;
    margin-top: 15px;
    line-height: 16px;
    padding-right: 10px;
    background-color:inherit;
}

.textaudio-playback-rate select {
    width: 100%;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
    border: 0;
    background-color:inherit;
}

.keyboard_navigation_active_element {
    z-index: 1;
}
</css><view>&lt;div class="wrapper-addon-textaudio"&gt;
    &lt;div class="textaudio-player"&gt;&lt;/div&gt;
    &lt;div class="textaudio-text slide-id-0"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="text-audio-loading-area"&gt;&lt;/div&gt;
</view><preview>&lt;div class="wrapper-addon-textaudio"&gt;
    &lt;div class="textaudio-player"&gt;&lt;/div&gt;
    &lt;div class="textaudio-text slide-id-0"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonTextAudio_create() {
    var presenter = function() {};

    presenter.markerWCAG = {};

    presenter.originalFile = {};
    presenter.vocabularyFile = {};
    presenter.eventBus = null;
    presenter.currentTimeAlreadySent = null;
    presenter.hasBeenStarted = false;
    presenter.isPlaying = false;
    presenter.globalIntervalNumber = 0;
    presenter.isVocabularyAudioLoaded = false;
    presenter.isVocabularyPlaying = false;
    presenter.buzzAudio = [];
    presenter.disabledTime = 0;
    presenter.audio = {};
    presenter.current_slide_data = {
        slide_id: -1,
        selection_id: -1
    };
    presenter.playerController = null;
    presenter.selectionId = undefined;
    presenter.playedByClick = false;
    presenter.addonID = null;
    presenter.fps = 10;
    presenter.previousSelectionId = -1;
    presenter.mouseData = {};
    presenter.slidesMade = false;
    presenter.keyboardControllerObject = null;
    presenter.isWCAGOn = false;
    presenter.speechTexts = null;
    presenter.volumeStep = 0.05;
    presenter.progressStep = 5;

    presenter.PLAYBACK_RATE_LIST = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0];
    presenter.playbackRate = 1.0;

    var controls = {
        CUSTOM: "Custom",
        BROWSER: "Browser",
        NONE: "None"
    };

    /**
     * play_interval_or_vocabulary - this option if for compatibility sake. If user had both
     * 'Individual fragment playback' and 'Vocabulary audio files playback' options selected the result was
     * different than can be now obtained by selecting one of the 'On Text Click Behavior' property option.
     */
    presenter.ALLOWED_CLICK_BEHAVIOUR = {
        play_from_the_moment: 'Play from the moment',
        play_interval: 'Play the interval',
        play_vocabulary_file: 'Play vocabulary audio file',
        play_vocabulary_interval: 'Play the interval from vocabulary file',
        play_interval_or_vocabulary: 'Play interval from base file or vocabulary audio file'
    };

    presenter.ERROR_CODES = {
        'M01': 'This addon needs at least 1 audio file.',
        'M02': 'Number of texts in the slide should be the same as number of time entities',
        'M03': 'Incorrectly defined period of time',
        'M04': 'Entry ends before start',
        'M05': 'Duplicated text for second',
        'M06': 'Error while downloading SRT file',
        'M07': 'Invalid crop time value',
        'SAF01': 'Property Vocabulary audio files cannot be empty',
        'SAF02': 'Number of Vocabulary audio files and time items must be the same',
        'SAF03': 'All values in property Vocabulary audio files has to be filled',
        'VI01': 'At least one vocabulary audio file have to be set.',
        'VI02': 'Number of parts in Vocabulary intervals have to be equal to sum of times periods defined in Slides property',
        'VI03': 'Vocabulary time intervals are not set'
    };

    presenter.DEFAULT_TTS_PHRASES = {
        PLAY_BUTTON: "Play button",
        PAUSE_BUTTON: "Pause button",
        STOP_BUTTON: "Stop button",
        AUDIO_SPEED_CONTROLLER: "Audio speed controller",
        VOLUME_CONTROLLER: "Volume controller",
        PROGRESS_BAR: "Progress bar"
    };

    presenter.CSS_CLASSES = {
        PLAY_BUTTON: "textaudio-play-btn",
        PAUSE_BUTTON: "textaudio-pause-btn",
        PLAY_PAUSE_BUTTON: "textaudio-play-pause-btn",
        STOP_BUTTON: "textaudio-stop-btn",
        AUDIO_SPEED_CONTROLLER: "textaudio-playback-rate",
        VOLUME_BTN: "textaudio-volume-btn",
        PROGRESS_BAR: "textaudio-progress-bar",
        ACTIVE: "active"
    };

    presenter.OPERATION_TYPE = {
        INCREASE: 'increase',
        DECREASE: 'decrease'
    };

    function isModuleEnabledDecorator (expectedValue) {
        return function (fn) {
            return function () {
                if (expectedValue === presenter.configuration.isEnabled) {
                    return fn.apply(this, arguments);
                }
            }
        }
    }

    presenter.getErrorObject = function AddonTextAudio_getErrorObject (ec) {
        return {
            isValid: false,
            errorCode: ec
        };
    };

    presenter.getCorrectObject = function AddonTextAudio_getCorrectObject (val) {
        return {
            isValid: true,
            value: val
        };
    };

    presenter.onEventReceived = function AddonTextAudio_onEventReceived (eventName, eventData) {
        if(eventData.value == 'dropdownClicked' &amp;&amp; !presenter.audio.playing &amp;&amp; !isTemporarilyPaused()) {
            presenter.audio.load();
        }
    };

    function isTemporarilyPaused() {
        return (presenter.audio.paused
            &amp;&amp; presenter.audio.readyState &gt; 2
            &amp;&amp; presenter.audio.currentTime &gt; 0
            &amp;&amp; !presenter.audio.ended
        );
    }

    presenter.showLoadingArea = function AddonTextAudio_showLoadingArea (clickAction) {
        if (clickAction === 'play_vocabulary_interval' &amp;&amp; presenter.buzzAudio.length === 0 &amp;&amp; !MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            presenter.$view.find('div.text-audio-loading-area').css('display','block');
        }
    };

    presenter.hideLoadingArea = function AddonTextAudio_hideLoadingArea () {
        var $loadingArea = presenter.$view.find('div.text-audio-loading-area');
        $loadingArea.css('display','none');
        $loadingArea.remove();
    };

    presenter.transposeDict = function AddonTextAudio_transposeDict (dict) {
        var transp = {};
        for (var key in dict) {
            if (dict.hasOwnProperty(key))
                transp[dict[key]] = key;
        }
        return transp;
    };

    presenter.startTimeMeasurement = function AddonTextAudio_startTimeMeasurement () {
        presenter.isPlaying = true;
        if (!presenter.audioClock) {
            presenter.audioClock = setInterval(function AddonTextAudio_audioClockInterval () { presenter.onTimeUpdateCallback(); }, 1000 / presenter.fps);
        }
    };

    presenter.stopTimeMeasurement = function AddonTextAudio_stopTimeMeasurement () {
        presenter.isPlaying = false;
        clearInterval(presenter.audioClock);
        presenter.audioClock = null;
    };

    presenter.startVocabularyTimeMeasurement = function AddonTextAudio_startVocabularyTimeMeasurement () {
        presenter.isVocabularyPlaying = true;
        if (!presenter.audioVocClock) {
            presenter.audioVocClock = setInterval(function AddonTextAudio_audioVOcClockInterval () { presenter.onTimeUpdateCallback(); }, 1000 / presenter.fps);
        }
    };

    presenter.stopVocabularyTimeMeasurement = function AddonTextAudio_stopVocabularyTimeMeasurement () {
        presenter.isVocabularyPlaying = false;
        clearInterval(presenter.audioVocClock);
        presenter.audioVocClock = null;
        presenter.clearSelection();
    };

    presenter.setPlayerController = function AddonTextAudio_setPlayerController (controller) {
        presenter.playerController = controller;
    };

    presenter.stopSingleAudioPlayer = function AddonTextAudio_stopSingleAudioPlayer () {
        presenter.removeMarkFromItems();

        for (var i=0; i&lt;presenter.buzzAudio.length; i++) {
            presenter.buzzAudio[i].stop();
        }
    };

    presenter.playSingleAudioPlayer = function AddonTextAudio_playSingleAudioPlayer (slideId, elementID) {
        presenter.stopSingleAudioPlayer();

        for (var i=0; i&lt;slideId; i++) {
            elementID += presenter.slidesLengths[i];
        }

        presenter.buzzAudio[elementID].play();
    };

    presenter.markItem = function AddonTextAudio_markItem (selectionId) {
        var selector = 'span[data-selectionid="[NUMBER]"]'.replace('[NUMBER]', selectionId);
        presenter.$view.find('.textaudio-text').find(selector).addClass('active');
    };

    presenter.removeMarkFromItems = function AddonTextAudio_removeMarkFromItems () {
        presenter.$view.find('span.active').removeClass('active');
    };

    presenter.upgradeIsDisabled = function (model) {
        var upgradedModel = {};

        $.extend(true, upgradedModel, model); // Deep copy of model object

        presenter.originalIsDisabledModel = upgradedModel['isDisabled'];
        if (upgradedModel['isDisabled'] === undefined) {
            upgradedModel['isDisabled'] = 'False';
        }

        return upgradedModel;
    };

    presenter.upgradeModel = function AddonTextAudio_upgradeModel (model) {
    	var upgradedModel = presenter.upgradeControls(model);
    	upgradedModel = presenter.upgradeIsDisabled(upgradedModel);
        upgradedModel = presenter.upgradeClickAction(upgradedModel);
        upgradedModel = presenter.upgradeEnablePlaybackSpeedControls(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    presenter.upgradeClickAction = function AddonTextAudio_upgradeClickAction (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (model.playPart != undefined || model.playSeparateFiles != undefined || model.separateFiles != undefined) {
            var playPart = ModelValidationUtils.validateBoolean(model.playPart),
                playSeparateFiles = ModelValidationUtils.validateBoolean(model.playSeparateFiles),
                clickAction = presenter.ALLOWED_CLICK_BEHAVIOUR.play_from_the_moment;

            if (playPart &amp;&amp; !playSeparateFiles) {
                clickAction = presenter.ALLOWED_CLICK_BEHAVIOUR.play_interval;
            } else if (!playPart &amp;&amp; playSeparateFiles) {
                clickAction = presenter.ALLOWED_CLICK_BEHAVIOUR.play_vocabulary_file;
            } else if (playPart &amp;&amp; playSeparateFiles) {
                clickAction = presenter.ALLOWED_CLICK_BEHAVIOUR.play_interval_or_vocabulary;
            }

            upgradedModel["clickAction"] = clickAction;
        }

        return upgradedModel;
    };

    presenter.upgradeControls = function AddonTextAudio_upgradeControls (model) {
    	var upgradedModel = {};

        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (typeof upgradedModel['controls'] == "undefined") {
        	if (upgradedModel['defaultControls'] == "True") {
        		upgradedModel['controls'] = "Browser";
        	} else {
        		upgradedModel['controls'] = "None";
        	}
        }

        return upgradedModel;
    };

    presenter.upgradeEnablePlaybackSpeedControls = function AddonTextAudio_upgradeEnablePlaybackSpeedControls (model) {
        var upgradedModel = {};

        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel.hasOwnProperty('enablePlaybackSpeedControls')) {
            upgradedModel['enablePlaybackSpeedControls'] = 'False';
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] =  '';
        }

        return upgradedModel;
    }

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Play"]) {
            upgradedModel["speechTexts"]["Play"] = {Play: ""};
        }
        if (!upgradedModel["speechTexts"]["Pause"]) {
            upgradedModel["speechTexts"]["Pause"] = {Pause: ""};
        }
        if (!upgradedModel["speechTexts"]["Stop"]) {
            upgradedModel["speechTexts"]["Stop"] = {Stop: ""};
        }
        if (!upgradedModel["speechTexts"]["AudioSpeedController"]) {
            upgradedModel["speechTexts"]["AudioSpeedController"] = {AudioSpeedController: ""};
        }
        if (!upgradedModel["speechTexts"]["ProgressBar"]) {
            upgradedModel["speechTexts"]["ProgressBar"] = {ProgressBar: ""};
        }
        if (!upgradedModel["speechTexts"]["VolumeController"]) {
            upgradedModel["speechTexts"]["VolumeController"] = {VolumeController: ""};
        }

        return upgradedModel;
    }

    presenter.setSpeechTexts = function (speechTexts) {
        presenter.speechTexts = {
            Play: presenter.DEFAULT_TTS_PHRASES.PLAY_BUTTON,
            Pause: presenter.DEFAULT_TTS_PHRASES.PAUSE_BUTTON,
            Stop: presenter.DEFAULT_TTS_PHRASES.STOP_BUTTON,
            AudioSpeedController: presenter.DEFAULT_TTS_PHRASES.AUDIO_SPEED_CONTROLLER,
            ProgressBar: presenter.DEFAULT_TTS_PHRASES.PROGRESS_BAR,
            VolumeController: presenter.DEFAULT_TTS_PHRASES.VOLUME_CONTROLLER
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Play: TTSUtils.getSpeechTextProperty(
                speechTexts.Play.Play,
                presenter.speechTexts.Play),
            Pause: TTSUtils.getSpeechTextProperty(
                speechTexts.Pause.Pause,
                presenter.speechTexts.Pause),
            Stop: TTSUtils.getSpeechTextProperty(
                speechTexts.Stop.Stop,
                presenter.speechTexts.Stop),
            AudioSpeedController: TTSUtils.getSpeechTextProperty(
                speechTexts.AudioSpeedController.AudioSpeedController,
                presenter.speechTexts.AudioSpeedController),
            ProgressBar: TTSUtils.getSpeechTextProperty(
                speechTexts.ProgressBar.ProgressBar,
                presenter.speechTexts.ProgressBar),
            VolumeController: TTSUtils.getSpeechTextProperty(
                speechTexts.VolumeController.VolumeController,
                presenter.speechTexts.VolumeController),
        };
    };

    presenter.getSlideNumber = function addonTextAudio_getSlideNumber () {
        return presenter.current_slide_data.slide_id + 1;
    };

    presenter.getEventObject = function AddonTextAudio_getEventObject (_item, _value, _score) {
        return {
            source : presenter.addonID,
            item: _item + "",
            value: _value + "",
            score: _score + ""
        };
    };

    presenter.createTimeUpdateEventData = function AddonTextAudio_createTimeUpdateEventData (data) {
        return presenter.getEventObject(presenter.getSlideNumber(), data.currentTime, "");
    };

    presenter.createOnEndEventData = function AddonTextAudio_createOnEndEventData () {
        return presenter.getEventObject("end", "", "");
    };

    presenter.createOnPlayEventData = function AddonTextAudio_createOnPlayEventData () {
        return presenter.getEventObject(presenter.getSlideNumber(), "playing", "");
    };

    presenter.createOnPauseEventData = function AddonTextAudio_createOnPauseEventData () {
        return presenter.getEventObject(presenter.getSlideNumber(), "stop", "");
    };

    presenter.getAudioCurrentTime = function AddonTextAudio_getAudioCurrentTime () {
        return presenter.audio.currentTime;
    };

    presenter.formatTime = function addonTextAudio_formatTime (seconds) {
        function addonTextAudio_addZero(v) { return (v &lt; 10 ? '0' : '') + v }

        var minutes = Math.floor(seconds / 60);
        var remainingSeconds = Math.floor(seconds % 60);

        return addonTextAudio_addZero(minutes) + ":" + addonTextAudio_addZero(remainingSeconds);
    };

    presenter.onLoadedMetadataCallback = function AddonTextAudio_onLoadedMetadataCallback () {
        var duration = parseInt(presenter.audio.duration, 10);
        duration = isNaN(duration) ? 0 : duration;
        presenter.displayTimer(0, duration);

        if (presenter.configuration.controls === controls.CUSTOM){
            presenter.$playerTime.html('0:00 / ' + presenter.formatTime(duration))
        }
    };

    presenter.sendEventAndSetCurrentTimeAlreadySent = function AddonTextAudio_sendEventAndSetCurrentTimeAlreadySent (eventData, currentTime) {
        presenter.eventBus.sendEvent('ValueChanged', eventData);
        presenter.currentTimeAlreadySent = currentTime;
    };

    presenter.sendOnEndEvent = function AddonTextAudio_sendOnEndEvent () {
        var eventData = presenter.createOnEndEventData();
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onTimeUpdateSendEventCallback = function AddonTextAudio_onTimeUpdateSendEventCallback () {
        var currentTime = presenter.formatTime(presenter.getAudioCurrentTime());
        if (currentTime !== presenter.currentTimeAlreadySent) { // to prevent duplicated value
            var eventData = presenter.createTimeUpdateEventData({'currentTime' : currentTime});
            presenter.sendEventAndSetCurrentTimeAlreadySent(eventData, currentTime);
        }
    };

    presenter.onTimeUpdateCallback = function AddonTextAudio_onTimeUpdateCallback () {
        var bar_width, currentTime, duration;

        if (presenter.isVocabularyPlaying) {
            if (presenter.vocabulary.getTime() &gt;= presenter.vocabulary_end) {
                presenter.stopVocabularyAudioPlaying();
            }
            return;
        }
        currentTime = presenter.audio.currentTime;
        duration = parseInt(presenter.audio.duration, 10);
        duration = isNaN(duration) ? 0 : duration;

        if (presenter.configuration.displayTime) {
            presenter.displayTimer(currentTime, duration);
        }

        if (presenter.configuration.controls === controls.CUSTOM){
            presenter.$playerTime.html(presenter.formatTime(currentTime) + ' / ' + presenter.formatTime(duration));
            bar_width = presenter.$progressWrapper.width() * currentTime / presenter.audio.duration;
            presenter.$progressBar.width(Math.round(bar_width));
            presenter.$progressSlider.css('left', Math.round(bar_width));
        }

        if(presenter.configuration.showSlides == "Show all slides"){
            presenter.changeSlideAll(currentTime);
        }else{
            presenter.changeSlide(currentTime);
        }

        if (presenter.stopClicked) {
            presenter.$view.find('span').removeClass('active');
            presenter.hasBeenStarted = false;
            presenter.stopClicked = false;
        }
    };

    presenter.changeVolumeClass = function addonTextAudio_changeVolumeClass (volume_btn, volume_class) {
        if (volume_btn.hasClass(volume_class)) {
            return;
        }
        for (var i=0; i&lt;=3; i++) {
            if (volume_btn.hasClass('textaudio-volume'+i)) {
                volume_btn.removeClass('textaudio-volume'+i);
            }
        }
        volume_btn.addClass(volume_class);
    };

    presenter.onVolumeChanged = function AddonTextAudio_onVolumeChanged () {
        var volume, volume_class;
        if (presenter.configuration.controls === controls.CUSTOM){
            volume = presenter.audio.volume;
            volume_class = '';
            presenter.$volumeControl.css('left', volume * presenter.$volumeLayer.width());
            if (volume &lt; 0.1) {
                volume_class = 'textaudio-volume0';
                if (volume &gt; 0) {
                    presenter.audio.volume = 0;
                }
            } else if (volume &lt; 0.4) {
                volume_class = 'textaudio-volume1';
            } else if (volume &lt; 0.7) {
                volume_class = 'textaudio-volume2';
            } else {
                volume_class = 'textaudio-volume3';
            }
            presenter.changeVolumeClass(presenter.$volumeBtn, volume_class);
        }
    };

    presenter.playPauseCallback = isModuleEnabledDecorator(true)(function AddonTextAudio_playPauseCallback () {
        if (presenter.$playPauseBtn.hasClass(presenter.CSS_CLASSES.PAUSE_BUTTON)) {
            presenter.pause();
        }
        else {
            presenter.play();
        }
    });

    presenter.displayTimer = function AddonTextAudio_displayTimer (current, duration) {
        presenter.$view.find('#currentTime').html(presenter.formatTime(current) + ' / ');
        presenter.$view.find('#durationTime').html(presenter.formatTime(duration));
    };

    presenter.goTo =  function AddonTextAudio_goTo (slide_id, selectionId) {
        if (slide_id &gt;= 0 || selectionId &gt;= 0) {
            var frame2go = presenter.configuration.slides[slide_id].Times[selectionId].start + 0.1;
            presenter.audio.currentTime = frame2go / presenter.fps;
        }
        presenter.play();
    };

    presenter.setPositionAndDimentions = function AddonTextAudio_setPositionAndDimentions (element, slide_id) {
        element.css('position', 'absolute');
        element.css('left', presenter.configuration.slides[slide_id].positionAndDimentions[0]+ 'px');
        element.css('top', presenter.configuration.slides[slide_id].positionAndDimentions[1]+ 'px');
        element.css('width', presenter.configuration.slides[slide_id].positionAndDimentions[2]+ 'px');
        element.css('height', presenter.configuration.slides[slide_id].positionAndDimentions[3]+ 'px');
    };

    presenter.removePositionAndDimentions = function AddonTextAudio_removePositionAndDimentions (element) {
        element.css('position', '');
        element.css('left', '');
        element.css('top', '');
        element.css('width', '');
        element.css('height', '');
    };

    presenter.makeSlide =  function AddonTextAudio_makeSlide (textWrapper, slide_id) {
        slide_id = parseInt(slide_id, 10);
        if (slide_id &lt; 0) {
            textWrapper.html('');
        } else {
            presenter.slidesSpanElements = [];

            if(presenter.configuration.showSlides == "Show all slides" &amp;&amp; slide_id &gt;=1){
                var textElement = $('&lt;div class="textaudio-text"&gt;&lt;/div&gt;');
                textElement.addClass('slide-id-'+slide_id);
                presenter.$view.find('.wrapper-addon-textaudio').append(textElement);
                textWrapper = presenter.$view.find('.slide-id-'+slide_id);
            }
            if(presenter.configuration.showSlides == "Show all slides"){
                textWrapper.append(presenter.configuration.slides[slide_id].html);
                if(presenter.configuration.slides[slide_id].positionAndDimentions != '' &amp;&amp; presenter.configuration.slides[slide_id].positionAndDimentions != undefined){
                    presenter.setPositionAndDimentions(textWrapper, slide_id);
                }
            }else{
                textWrapper.html(presenter.configuration.slides[slide_id].html);
                if(presenter.configuration.slides[slide_id].positionAndDimentions != '' &amp;&amp; presenter.configuration.slides[slide_id].positionAndDimentions != undefined){
                    presenter.setPositionAndDimentions(textWrapper, slide_id);
                }else{
                    presenter.removePositionAndDimentions(textWrapper);
                }
            }
            textWrapper.attr('data-slideId', slide_id);
            textWrapper.find("span[class^='textelement']").each(function AddonTextAudio_textWrapperTextElementEach () {
                var that = this;
                if(!presenter.configuration.isClickDisabled){
                    presenter.slidesSpanElements.push(that);
                    $(that).on('click', isModuleEnabledDecorator(true)(function addonTextAudio_textWrapperTextElementOnClick (e) {
                        e.stopPropagation();

                        var isVocabularyInterval = presenter.configuration.clickAction == 'play_vocabulary_interval';
                        var isLoaded = presenter.isVocabularyAudioLoaded || presenter.buzzAudio.length !== 0;
                        if (isVocabularyInterval &amp;&amp; !MobileUtils.isMobileUserAgent(navigator.userAgent) &amp;&amp; !isLoaded) {
                            return false;
                        }

                        presenter.playedByClick = true;
                        presenter.selectionId = parseInt($(this).attr('data-selectionId'), 10);

                        switch (presenter.configuration.clickAction) {
                            case 'play_vocabulary_interval':
                                if (presenter.isVocabularyPlaying || !presenter.isPlaying) {
                                    var intervalId = parseInt($(this).attr('data-intervalId'), 10);
                                    var frame = presenter.configuration.vocabularyIntervals[intervalId];

                                    if (presenter.isPlaying) {
                                        presenter.vocabulary.stop();
                                    }
                                    presenter.clearSelection();
                                    presenter.vocabulary.setTime(frame.start / presenter.fps);
                                    presenter.vocabulary_end = frame.end / presenter.fps;
                                    presenter.vocabulary.play();
                                    presenter.markItem(presenter.selectionId);
                                    break;
                                }
                            case 'play_interval_or_vocabulary':
                            case 'play_vocabulary_file':
                                if (!presenter.isPlaying) {
                                    presenter.pause();
                                    presenter.playSingleAudioPlayer(slide_id, presenter.selectionId);
                                    presenter.markItem(presenter.selectionId);
                                    break;
                                }
                            case 'play_interval':
                            case 'play_from_the_moment':
                                presenter.play();

                                if ($(this).hasClass("tmp-active")) {
                                    $(this).removeClass("tmp-active");
                                    $(this).addClass("active");
                                }

                                if (MobileUtils.isSafariMobile(navigator.userAgent)) {
                                    function AddonTextAudio_fun() {
                                        if (slide_id &gt;= 0 || presenter.selectionId &gt;= 0) {
                                            var frame2go = presenter.configuration.slides[slide_id].Times[presenter.selectionId].start;
                                            presenter.audio.currentTime = frame2go / presenter.fps;
                                        }
                                        presenter.audio.removeEventListener("playing", AddonTextAudio_fun, false);
                                    }
                                    if (presenter.hasBeenStarted) {
                                        presenter.pause();
                                        presenter.goTo(slide_id, presenter.selectionId);
                                    } else {
                                        presenter.audio.addEventListener("playing", AddonTextAudio_fun, false);
                                    }
                                } else {
                                    presenter.goTo(slide_id, presenter.selectionId);
                                }
                        }
                    }));
                }
            });

        }
    };

    presenter.highlightSelection = function AddonTextAudio_highlightSelection (textWrapper, selection_id) {
        textWrapper.find('span').each(function () {
            $(this).removeClass('active');
        });
        if (selection_id &gt;= 0) {
            textWrapper.find('span.textelement' + selection_id).addClass('active');
        }
    };

    presenter.areSlidesEqual = function addonTextAudio_areSlidesEqual (slide1, slide2) {
        return slide1.slide_id === slide2.slide_id &amp;&amp; slide1.selection_id === slide2.selection_id;
    };

    presenter.highlightSelectionAll = function AddonTextAudio_highlightSelectionAll (textWrapper, selection_id) {
        presenter.$view.find('.textaudio-text').find('span').each(function () {
            $(this).removeClass('active');
        });

        if(presenter.currentSlide &gt;= 1){
            textWrapper = presenter.$view.find('.slide-id-'+presenter.currentSlide);
        }

        if (selection_id &gt;= 0) {
            textWrapper.find('span.textelement' + selection_id).addClass('active');
        }
    };

    presenter.changeSlideAll = function AddonTextAudio_changeSlideAll (currentTime) {
        currentTime = Math.round(currentTime * presenter.fps);
        var frames_array = presenter.configuration.frames;
        var isCurrentTimeInRange = currentTime &lt; frames_array.length;
        var slide_data = {
            slide_id: isCurrentTimeInRange ? frames_array[currentTime].slide_id : -1,
            selection_id: isCurrentTimeInRange ? frames_array[currentTime].selection_id : 0
        };
        presenter.currentSlide = slide_data.slide_id;
        if (!presenter.hasBeenStarted) {
            slide_data.selection_id = -1;
        }
        var difference = slide_data.selection_id - presenter.previousSelectionId;
        if (difference &gt; 1 &amp;&amp; !presenter.playedByClick &amp;&amp; presenter.previousSelectionId != -1) {
            slide_data.selection_id -= difference - 1;
        }
        presenter.previousSelectionId = slide_data.selection_id;
        presenter.changeSlideFromDataAll(slide_data);
    };

    presenter.changeSlideFromDataAllPreview = function AddonTextAudio_changeSlideFromDataAllPreview (slide_data) {
        const textWrapper = presenter.$view.find(".slide-id-0");
        if (!presenter.areSlidesEqual(slide_data, presenter.current_slide_data)) {
            const currentSelId = presenter.current_slide_data.selection_id;
            if (presenter.configuration.playPart &amp;&amp; currentSelId !== -1 &amp;&amp; presenter.selectionId === currentSelId) {
                presenter.pause();
            }
            if (slide_data.slide_id !== presenter.current_slide_data.slide_id &amp;&amp; !presenter.slidesMade) {
                presenter.makeSlide(textWrapper, slide_data.slide_id);
            }
            presenter.current_slide_data = slide_data;
            presenter.playedByClick = false;
        }
    };

    presenter.changeSlideFromDataAll = function AddonTextAudio_changeSlideFromDataAll (slide_data) {
        var textWrapper = presenter.$view.find(".slide-id-0");
        if (!presenter.areSlidesEqual(slide_data, presenter.current_slide_data)) {
            var blockHighlight = false;
            var currentSelId = presenter.current_slide_data.selection_id;
            if (presenter.configuration.playPart &amp;&amp; currentSelId !== -1 &amp;&amp; presenter.selectionId === currentSelId) {
                presenter.pause();
                blockHighlight = true;
            }
            if (slide_data.slide_id !== presenter.current_slide_data.slide_id &amp;&amp; !presenter.slidesMade) {
                presenter.makeSlide(textWrapper, slide_data.slide_id);
            }
            presenter.highlightSelectionAll(textWrapper, slide_data.selection_id);

            if (blockHighlight) {
                textWrapper.find('span').each(function AddonTextAudio_textWrapperSpanEach2 () {
                    if ($(this).hasClass('active')) {
                        $(this).removeClass('active');
                        $(this).addClass("tmp-active");
                    }
                });
            }
            presenter.current_slide_data = slide_data;
            presenter.playedByClick = false;
        } else if (presenter.$view.find('.active').length === 0) {
            presenter.highlightSelectionAll(textWrapper, slide_data.selection_id);
        }
    };

    presenter.changeSlideFromData = function AddonTextAudio_changeSlideFromData (slide_data) {
        var textWrapper = presenter.$view.find(".wrapper-addon-textaudio .textaudio-text");

        if (!presenter.areSlidesEqual(slide_data, presenter.current_slide_data)) {
            var blockHighlight = false;

            var currentSelId = presenter.current_slide_data.selection_id;
            if (presenter.configuration.playPart &amp;&amp; currentSelId !== -1 &amp;&amp; presenter.selectionId === currentSelId) {
                presenter.pause();
                blockHighlight = true;
            }

            if (slide_data.slide_id != presenter.current_slide_data.slide_id &amp;&amp; slide_data.slide_id !== -2) {
                presenter.makeSlide(textWrapper, slide_data.slide_id);
                presenter.actualizeKeyboardControllerElements();
            }
            presenter.highlightSelection(textWrapper, slide_data.selection_id);

            if (blockHighlight) {
                textWrapper.find('span').each(function AddonTextAudio_textWrapperSpanEach3 () {
                    if ($(this).hasClass('active')) {
                        $(this).removeClass('active');
                        $(this).addClass("tmp-active");
                    }
                });
            }

            presenter.current_slide_data = slide_data;
            presenter.playedByClick = false;
        } else {
            if (presenter.$view.find('.active').length === 0) {
                presenter.highlightSelection(textWrapper, slide_data.selection_id);
            }
        }
        presenter.addHoverHandlers();
    };

    presenter.addHoverHandlers = function AddonTextAudio_addHoverHandlers () {
        var textElements = presenter.$view.find("span[class^='textelement']");
        textElements.on('mouseover', function(){
            var index = this.getAttribute('data-selectionid');
            presenter.$view.find("span.textelement" + index).addClass("hover");
        });
        textElements.on('mouseout', function(){
            var index = this.getAttribute('data-selectionid');
            presenter.$view.find("span.textelement" + index).removeClass("hover");
        });
    }

    presenter.changeSlide = function AddonTextAudio_changeSlide (currentTime) {
        currentTime = Math.round(currentTime * presenter.fps);

        var frames_array = presenter.configuration.frames;
        var isCurrentTimeInRange = currentTime &lt; frames_array.length;

        var slide_data = {
            slide_id: isCurrentTimeInRange ? frames_array[currentTime].slide_id : -1,
            selection_id: isCurrentTimeInRange ? frames_array[currentTime].selection_id : 0
        };

        if (isCurrentTimeInRange) {
            slide_data.slide_id = frames_array[currentTime].slide_id === -1 ? -2 : slide_data.slide_id;
        }

        if (!presenter.hasBeenStarted) {
            slide_data.selection_id = -1;
        }

        var difference = slide_data.selection_id - presenter.previousSelectionId;
        if (difference &gt; 1 &amp;&amp; !presenter.playedByClick &amp;&amp; presenter.previousSelectionId != -1) {
            slide_data.selection_id -= difference - 1;
        }

        presenter.previousSelectionId = slide_data.selection_id;
        presenter.changeSlideFromData(slide_data);
    };

    presenter.progressMouseDownCallback = isModuleEnabledDecorator(true)(function AddonTextAudio_progressMouseDownCallback (event) {
        if ($(event.target).hasClass('textaudio-slider-btn')) {
            presenter.mouseData.oldPosition = event.pageX;
            presenter.mouseData.isMouseDragged = true;
            presenter.mouseData.playedBeforeDragging = !presenter.audio.paused;
            if (!presenter.audio.paused) {
                presenter.pause();
            }
        }
    });

    presenter.progressMouseUpCallback = function AddonTextAudio_progressMouseUpCallback () {
        var duration;
        if (presenter.mouseData.isMouseDragged) {
            duration = presenter.audio.duration;
            duration = isNaN(duration) ? 0 : duration;
            presenter.audio.currentTime = duration * presenter.$progressBar.width() / presenter.$progressWrapper.width();
            presenter.mouseData.isMouseDragged = false;
            presenter.mouseData.oldPosition = 0;
            if (presenter.mouseData.playedBeforeDragging) {
                presenter.play();
            }
        }
    };

    presenter.progressMouseMoveCallback = isModuleEnabledDecorator(true)(function AddonTextAudio_progressMouseMoveCallback (event) {
        var relativeDistance, barWidth, oldWidth;
        if (presenter.mouseData.isMouseDragged){
            relativeDistance = event.pageX - presenter.mouseData.oldPosition;
            barWidth = 0;
            oldWidth = presenter.$progressBar.width();
            if (oldWidth + relativeDistance &lt; presenter.$progressWrapper.width()) {
                barWidth = oldWidth + relativeDistance;
            }
            else {
                barWidth = presenter.$progressWrapper.width();
            }
            presenter.$progressBar.width(barWidth);
            presenter.$progressSlider.css('left',Math.round(barWidth));
            presenter.mouseData.oldPosition = event.pageX;
        }
    });

    presenter.isMoreThanOneFingerGesture = function addonTextAudio_isMoreThanOneFingerGesture (event) {
        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];
        if (event.hasOwnProperty('touches'))
            touchPoints = event.touches;
        return touchPoints.length&gt; 1;
    };

    presenter.progressTouchStartCallback = isModuleEnabledDecorator(true)(function addonTextAudio_progressTouchStartCallback (event) {
        if (presenter.isMoreThanOneFingerGesture(event)) return;
        var touch, touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0];
        else
            touch = touchPoints[0];
        presenter.progressMouseDownCallback(touch);
    });

    presenter.progressTouchEndCallback =  function AddonTextAudio_progressTouchEndCallback () {
        presenter.progressMouseUpCallback();
    };

    presenter.progressTouchMoveCallback = isModuleEnabledDecorator(true)(function AddonTextAudio_progressTouchMoveCallback (event) {
        if (presenter.isMoreThanOneFingerGesture(event)) return;

        var touch;
        var touchPoints = (typeof event.changedTouches != 'undefined') ? event.changedTouches : [event];

        if (event.hasOwnProperty('touches'))
            touch = event.touches[0];
        else
            touch = touchPoints[0];
        presenter.progressMouseMoveCallback(touch);
    });

    presenter.attachProgressListeners = function AddonTextAudio_attachProgressListeners () {
        presenter.isMobileDevice = MobileUtils.isMobileUserAgent(navigator.userAgent) || MobileUtils.isEventSupported('touchend');
        if (MobileUtils.isWindowsMobile(window.navigator)) {
            presenter.$progressWrapper[0].addEventListener("MSPointerDown", presenter.progressTouchStartCallback , false);
            presenter.$progressWrapper[0].addEventListener("MSPointerUp", presenter.progressTouchEndCallback, false);
            presenter.$progressWrapper[0].addEventListener("MSPointerMove", presenter.progressTouchMoveCallback, false);
        }
        else if (presenter.isMobileDevice) {
            presenter.$progressWrapper[0].ontouchstart=presenter.progressTouchStartCallback ;
            presenter.$customPlayer[0].ontouchend=presenter.progressTouchEndCallback;
            presenter.$progressWrapper[0].ontouchmove=presenter.progressTouchMoveCallback;
        }
        else {
            presenter.$progressWrapper.on('mousedown', presenter.progressMouseDownCallback);
            presenter.$progressWrapper.on('mouseup', presenter.progressMouseUpCallback);
            presenter.$progressWrapper.on('mousemove', presenter.progressMouseMoveCallback);
        }
    };

    presenter.toogleVolumeLayer = isModuleEnabledDecorator(true)(function AddonTextAudio_toogleVolumeLayer (){
        presenter.onVolumeChanged();
        presenter.$volumeLayer.toggle();
        presenter.$playerTime.toggle();
    });

    presenter.createHtmlPlayer = function AddonTextAudio_createHtmlPlayer () {
        var $volumeControlBackground;

        presenter.$customPlayer = $('&lt;div&gt;').
            addClass('textaudioplayer');

        presenter.$playPauseBtn = $('&lt;div&gt;').
            addClass(presenter.CSS_CLASSES.PLAY_PAUSE_BUTTON).
            addClass(presenter.CSS_CLASSES.PLAY_BUTTON).
            on('click', presenter.playPauseCallback);

        presenter.$customPlayer.append(presenter.$playPauseBtn);

        presenter.$stopBtn = $('&lt;div&gt;').
            addClass(presenter.CSS_CLASSES.STOP_BUTTON).
            on('click', presenter.stop);

        presenter.$customPlayer.append(presenter.$stopBtn);

        presenter.$progressWrapper = $('&lt;div&gt;').
            addClass('textaudio-progress-bar');

        presenter.$progressBar = $('&lt;div&gt;').
            addClass('textaudio-bar');

        presenter.$progressSlider = $('&lt;div&gt;').
            addClass('textaudio-slider-btn');

        presenter.attachProgressListeners();

        presenter.$progressWrapper.
            append(presenter.$progressBar).
            append(presenter.$progressSlider);

        presenter.$customPlayer.append(presenter.$progressWrapper);

        if (!MobileUtils.isSafariMobile(navigator.userAgent)) {
            presenter.$volumeBtn = $('&lt;div&gt;').
                addClass('textaudio-volume-btn').
                on('click', presenter.toogleVolumeLayer);

            presenter.$volumeLayer = $('&lt;div&gt;').
                addClass('textaudio-volume-layer').
                on('click', presenter.volumeLayerOnClick).
                hide();

            $volumeControlBackground = $('&lt;div&gt;').
                addClass('textaudio-volume-control-background');

            presenter.$volumeControl = $('&lt;div&gt;').
                addClass('textaudio-volume-control');

            presenter.$volumeLayer.
                append($volumeControlBackground).
                append(presenter.$volumeControl);

            presenter.$customPlayer.
                append(presenter.$volumeBtn).
                append(presenter.$volumeLayer);
        }

        if (presenter.configuration.enablePlaybackSpeedControls) {
            presenter.$playbackRateControls = $('&lt;div&gt;').
                addClass(presenter.CSS_CLASSES.AUDIO_SPEED_CONTROLLER).
                append(createPlaybackOptions());
            displayPlaybackOptions();
            presenter.$customPlayer.append(presenter.$playbackRateControls);
        }

        presenter.$playerTime = $('&lt;div&gt;').
            addClass('textaudio-player-time').
            text('00:00 / --:--');

        presenter.$customPlayer.append(presenter.$playerTime);

        presenter.$audioWrapper.append(presenter.$customPlayer);
    };

    function createPlaybackOptions() {
        var $select = $('&lt;select&gt;');
        var playingSpeedOptions = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2];
        
        playingSpeedOptions.forEach(playingOption =&gt; {
            var $option = $('&lt;option&gt;');
            $option.text(playingOption);
            $option.attr('value', playingOption);
            if (playingOption === 1.0) {
                $option.attr('selected', 'selected');
            }
            $select.append($option);
        });

        $select.on('change', function () {
            presenter.setPlaybackRate($select.val());
        });
        
        return $select;
    }

    function displayPlaybackOptions() {
        var playingSpeedOptions = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2];
        if (presenter.$playbackRateControls) {
            var $selected = presenter.$playbackRateControls.find('select');
            if ($selected.val() === presenter.playbackRate) {
                if (playingSpeedOptions.indexOf(presenter.playbackRate) !== -1) {
                    $selected.find('.custom-option').remove();
                }
                return;
            }
            $selected.find('.custom-option').remove();
            if (playingSpeedOptions.indexOf(presenter.playbackRate) !== -1) {
                $selected.val(presenter.playbackRate);
            } else {
                var $customOption = $('&lt;option&gt;');
                $customOption.text(presenter.playbackRate);
                $customOption.attr('value', presenter.playbackRate);
                $customOption.addClass('custom-option');
                $selected.append($customOption);
                $selected.val(presenter.playbackRate);
            }
        }
    }

    presenter.volumeLayerOnClick = isModuleEnabledDecorator(true)(function AddonTextAudio_volumeLayerOnClick (e) {
        presenter.audio.volume = e.offsetX / $(this).width();
    });

    presenter.createView = function AddonTextAudio_createView (view, model, isPreview) {
        presenter.$view.bind('click', function (event) {
            event.stopPropagation();
        });
        presenter.originalFile.mp3 = model.mp3;
        presenter.originalFile.ogg = model.ogg;

        presenter.audio = document.createElement('audio');
        presenter.$audioWrapper = presenter.$view.find(".wrapper-addon-textaudio .textaudio-player");

        if (presenter.configuration.controls === controls.BROWSER) {
            presenter.audio.setAttribute("controls", "controls");
            presenter.audio.setAttribute("preload", "auto");
        } else if (presenter.configuration.controls === controls.CUSTOM) {
            presenter.createHtmlPlayer();
        }

        var currentTime = document.createElement("span");
        var durationTime = document.createElement("span");
        $(currentTime).attr("id", "currentTime").addClass('current-time');
        $(durationTime).attr("id", "durationTime").addClass('duration-time');

        presenter.$audioWrapper.append(presenter.audio);
        if (presenter.configuration.displayTime) {
            presenter.$audioWrapper.append(currentTime).append(durationTime);
            presenter.audio.addEventListener('loadeddata', presenter.onLoadedMetadataCallback, false);
        }

        presenter.handleChangingSlides(isPreview);
        presenter.slidesMade = true;

        if (!isPreview) {
            presenter.audio.addEventListener('timeupdate', presenter.onTimeUpdateSendEventCallback, false);
            presenter.audio.addEventListener('playing', presenter.onAudioPlaying, false);
            presenter.audio.addEventListener('play', presenter.onAudioPlay, false);
            presenter.audio.addEventListener('pause', presenter.onAudioPause, false);

            presenter.configuration.isEnabled = true;   // At start addon is always enabled, so we need to reset flag and set correct value.
            if (presenter.isEnabledByDefault) {
                presenter.enable();
            } else {
                presenter.disable();
            }
        }

        Object.defineProperty(presenter.audio, 'playing', {
            get: function () {
                return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
            }
        });
    };

    presenter.handleChangingSlides = function (isPreview) {
        if (presenter.configuration.showSlides === "Show all slides") {
            const frames = presenter.filterFrames(presenter.configuration.frames);
            const changeSlideFunction = isPreview
                ? presenter.changeSlideFromDataAllPreview
                : presenter.changeSlideFromDataAll;
            for (let i = 0; i &lt; frames.length; i++) {
                changeSlideFunction(frames[i]);
            }
            if (!isPreview) {
                presenter.addHoverHandlers();
            }
            presenter.$view.find('.textaudio-text span').removeClass('active');
        } else {
            presenter.changeSlide(presenter.getFirstSlideStartTime());
        }
    }

    presenter.getFirstSlideStartTime = function () {
        return presenter.configuration.slides[0].Times[0].start;
    }

    presenter.filterFrames = function (frames) {
        let lastAddedFrame = null;
        return frames.filter(function (frame) {
            if (frame.slide_id &lt; 0 || frame.selection_id &lt; 0) {
                return false;
            }
            if (lastAddedFrame === null || !presenter.areSlidesEqual(lastAddedFrame, frame)) {
                lastAddedFrame = frame;
                return true;
            }
            return false;
        })
    }

    presenter.onAudioPlaying = function AddonTextAudio_onAudioPlaying () {
        presenter.hasBeenStarted = true;
    };

    presenter.onAudioPlay =  function AddonTextAudio_onAudioPlay () {
        if (presenter.isVocabularyPlaying) {
            presenter.vocabulary.pause();
        }
        if (!presenter.playedByClick) {
            presenter.selectionId = undefined;
        }
        presenter.eventBus.sendEvent('ValueChanged', presenter.createOnPlayEventData());
        presenter.startTimeMeasurement();
    };

    presenter.onAudioPause = function AddonTextAudio_onAudioPause () {
        presenter.stopTimeMeasurement();
        presenter.eventBus.sendEvent('ValueChanged', presenter.createOnPauseEventData())
    };

    presenter.attachEventListeners = function AddonTextAudio_attachEventListeners () {
        presenter.audio.addEventListener('loadeddata', presenter.onLoadedMetadataCallback, false);
        presenter.audio.addEventListener('timeupdate', presenter.onTimeUpdateCallback, false);
        presenter.audio.addEventListener('volumechange', presenter.onVolumeChanged, false);
        presenter.audio.addEventListener('ended', presenter.onEnded, false);
        presenter.audio.addEventListener('click', presenter.onAudioClick, false);
    };

    presenter.onEnded = function AddonTextAudio_onEnded () {
        if (presenter.configuration.enableLoop) {
            presenter.currentTime = 0;
            presenter.play();
        } else {
            presenter.executeOnEndEvent();
            presenter.sendOnEndEvent();
            presenter.stop();
            presenter.$view.find(".wrapper-addon-textaudio .textaudio-text :last-child").removeClass('active');
        }

        presenter.playedByClick = false;
    };

    presenter.onAudioClick = function AddonTextAudio_onAudioClick (event) {
        event.stopPropagation();
    };

    presenter.canPlayMp3 = function () {
        return presenter.audio.canPlayType &amp;&amp; "" != presenter.audio.canPlayType('audio/mpeg');
    };

    presenter.canPlayOgg = function () {
        return presenter.audio.canPlayType &amp;&amp; "" != presenter.audio.canPlayType('audio/ogg; codecs="vorbis"');
    };

    presenter.setAudioSrc = function () {
        var canPlayMp3 = presenter.canPlayMp3();
        var canPlayOgg = presenter.canPlayOgg();

        if (canPlayMp3) {
            presenter.audio.setAttribute("src", presenter.originalFile .mp3);
        } else if (canPlayOgg) {
            presenter.audio.setAttribute("src", presenter.originalFile .ogg);
        }
    };

    presenter.loadFiles = function AddonTextAudio_loadFiles () {
        if (presenter.audio.canPlayType) {
            presenter.setAudioSrc();
            if (presenter.configuration.clickAction == 'play_vocabulary_interval') {
                presenter.vocabulary = new buzz.sound([
                    presenter.configuration.vocabulary_mp3,
                    presenter.configuration.vocabulary_ogg
                ]);
                presenter.vocabulary.bind('canplaythrough', function AddonTextAudio_vocabularyCanPlayThrough () {
                    presenter.vocabulary.unbind('canplaythrough');
                    presenter.isVocabularyAudioLoaded = true;
                    presenter.hideLoadingArea();
                }, false);
                presenter.vocabulary.bind('ended', function AddonTextAudio_vocabularyEnded () {
                    presenter.clearSelection();
                });
                presenter.vocabulary.bind('play', function AddonTextAudio_vocabularyPlay () {
                    if (!presenter.playedByClick) {
                        presenter.selectionId = undefined;
                    }
                    presenter.startVocabularyTimeMeasurement();
                }, false);
                presenter.vocabulary.bind('pause', function AddonTextAudio_vocabularyPause () {
                    presenter.stopVocabularyTimeMeasurement();
                }, false);
            }
        } else {
            var infoSpan = document.createElement('span');
            infoSpan.innerHTML = "Your browser doesn't support audio.";
            presenter.audio.appendChild(infoSpan);
        }

        presenter.audio.load();
        presenter.attachEventListeners();
    };

    presenter.createSeparateAudioFiles = function AddonTextAudio_createSeparateAudioFiles (audioFiles) {
        for (var i=0; i&lt;audioFiles.length; i++) {
            var localBuzz = new buzz.sound([
                audioFiles[i].mp3,
                audioFiles[i].ogg
            ]);

            localBuzz.bind('ended', function AddonTextAudio_localBuzzEnded () {
                presenter.clearSelection();
            });

            presenter.buzzAudio.push(localBuzz);
        }
    };

    presenter.run = function AddonTextAudio_run (view, model) {
        presenter.addonID = model.ID;
        presenter.initialize(view, model, false);

        presenter.eventBus = presenter.playerController.getEventBus();
        presenter.isLoaded = false;
        if (presenter.configuration.isValid) {
            presenter.audio.addEventListener("loadeddata", presenter.onAudioLoadedData);
        }

        presenter.eventBus.addEventListener('ValueChanged', this);
    };

    presenter.onAudioLoadedData = function AddonTextAudio_onAudioLoadedData () {
        presenter.isLoaded = true;
    };

    presenter.destroy = function AddonTextAudio_destroy (event) {
        if (event.target !== presenter.view) {
            return;
        }

        if (presenter.$customPlayer) {
            presenter.$customPlayer[0].ontouchend = null;
            presenter.$customPlayer = null;
        }
        if (presenter.$playPauseBtn) {
            presenter.$playPauseBtn.off();
            presenter.$playPauseBtn = null;
        }
        if (presenter.$stopBtn) {
            presenter.$stopBtn.off();
            presenter.$stopBtn = null;
        }
        if (presenter.$progressWrapper) {
            presenter.$progressWrapper[0].removeEventListener("MSPointerDown", presenter.progressTouchStartCallback , false);
            presenter.$progressWrapper[0].removeEventListener("MSPointerUp", presenter.progressTouchEndCallback, false);
            presenter.$progressWrapper[0].removeEventListener("MSPointerMove", presenter.progressTouchMoveCallback, false);
            presenter.$progressWrapper[0].ontouchstart = null;
            presenter.$progressWrapper[0].ontouchmove = null;
            presenter.$progressWrapper.off();
            presenter.$progressWrapper = null;
        }
        if (presenter.$progressBar) {
            presenter.$progressBar = null;
        }
        if (presenter.$progressSlider) {
            presenter.$progressSlider = null;
        }
        if (presenter.$volumeBtn) {
            presenter.$volumeBtn.off();
            presenter.$volumeBtn = null;
        }
        if (presenter.$volumeLayer) {
            presenter.$volumeLayer.off();
            presenter.$volumeLayer = null;
        }
        if (presenter.$volumeControl) {
            presenter.$volumeControl = null;
        }
        if (presenter.$playerTime) {
            presenter.$playerTime = null;
        }

        if (presenter.audio) {
            presenter.audio.pause();
            presenter.audio.removeEventListener('loadeddata', presenter.onLoadedMetadataCallback, false);
            presenter.audio.removeEventListener('timeupdate', presenter.onTimeUpdateSendEventCallback, false);
            presenter.audio.removeEventListener('playing', presenter.onAudioPlaying, false);
            presenter.audio.removeEventListener('play', presenter.onAudioPlay, false);
            presenter.audio.removeEventListener('pause', presenter.onAudioPause, false);
            presenter.audio.removeEventListener('timeupdate', presenter.onTimeUpdateCallback, false);
            presenter.audio.removeEventListener('volumechange', presenter.onVolumeChanged, false);
            presenter.audio.removeEventListener('ended', presenter.onEnded, false);
            presenter.audio.removeEventListener('click', presenter.onAudioClick, false);
            presenter.audio.removeEventListener("loadeddata", presenter.onAudioLoadedData);
            presenter.audio.removeEventListener("loadeddata", presenter.stopAudioLoadedData);
            presenter.audio.removeEventListener("loadeddata", presenter.playAudioLoadedData);
            presenter.audio.setAttribute('src', '');
            presenter.audio.removeAttribute('src');
            presenter.audio.load();
        }

        if (presenter.vocabulary) {
            presenter.vocabulary.unbind('ended play pause canplaythrough');
            presenter.stopVocabularyAudioPlaying();
            presenter.vocabulary = null;
        }

        presenter.buzzAudio.forEach(function (singleBuzzAudio, index) {
            singleBuzzAudio.unbind('ended play pause canplaythrough');
            presenter.buzzAudio[index] = null;
        });

        presenter.slidesSpanElements.forEach(function (spanElement, index) {
            $(spanElement).off();
            presenter.slidesSpanElements[index] = null;
        });

        try {
            clearInterval(presenter.audioClock);
        } catch (e) {}

        try {
            clearInterval(presenter.audioVocClock);
        } catch (e) {}

        presenter.$audioWrapper = null;
        presenter.originalFile = null;
        presenter.vocabularyFile = null;
        presenter.eventBus = null;
        presenter.currentTimeAlreadySent = null;
        presenter.buzzAudio = null;
        presenter.audio = null;
        presenter.current_slide_data = null;
        presenter.playerController = null;
        presenter.mouseData = null;
        presenter.audioClock = null;
        presenter.audioVocClock = null;
        presenter.configuration = null;
        presenter.slidesSpanElements = null;
        presenter.$view.unbind();
        presenter.view = null;
        presenter.$view = null;
    };

    presenter.stopVocabularyAudioPlaying = function AddonTextAudio_stopVocabularyAudioPlaying() {
        presenter.vocabulary.setTime(0);
        presenter.vocabulary.pause();
    };

    presenter.createPreview = function AddonTextAudio_createPreview (view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.initialize = function AddonTextAudio_initialize (view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);

        buzz.defaults.preload = 'auto';
        buzz.defaults.autoplay = false;
        buzz.defaults.loop = false;

        presenter.slidesSpanElements = [];

        var upgradedModel = presenter.upgradeModel(model);
        presenter.setSpeechTexts(upgradedModel["speechTexts"]);
        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            delete presenter.play;
            delete presenter.stop;
            delete presenter.pause;
            delete presenter.show;
            delete presenter.hide;
            delete presenter.executeCommand;
            return;
        }

        presenter.isVisibleByDefault = presenter.configuration.isVisible;
        presenter.isEnabledByDefault = presenter.configuration.isEnabled;

        presenter.createView(view, upgradedModel, isPreview);

        if (!isPreview) {
            presenter.loadFiles();

            if (presenter.configuration.playSeparateFiles) {
                presenter.createSeparateAudioFiles(presenter.configuration.separateFiles);
            }

            presenter.showLoadingArea(presenter.configuration.clickAction);

            presenter.buildKeyboardController();
        }

        if (presenter.addonID != null) {
            MutationObserverService.createDestroyObserver(presenter.addonID, presenter.destroy, presenter.view);
            MutationObserverService.setObserver();
        }
    };

    presenter.roundTimeEntry = function addonTextAudio_roundTimeEntry (time_entry) {
        var time = time_entry.replace(',', '.').split(':'),
            minutes = time[0],
            seconds = parseFloat(time[1]).toFixed(1).split('.');

        if (seconds[0].length &lt; 2) {
            seconds[0] = '0' + seconds[0];
        }

        return minutes + ':' + seconds[0] + '.' + seconds[1];
    };

    presenter.toFrames = function addonTextAudio_toFrames (time_entry) {
        time_entry =  presenter.roundTimeEntry(time_entry);

        var entry = time_entry.split(':');
        var minutes = parseInt(entry[0], 10);
        var seconds = 0;
        var decyseconds = 0;
        if (entry[1].indexOf('.') &gt; -1) {
            var seconds_with_decyseconds = entry[1].split('.');
            seconds = parseInt(seconds_with_decyseconds[0], 10);
            decyseconds = parseInt(seconds_with_decyseconds[1], 10);
            if (decyseconds.toString().length &gt; 1) {
                decyseconds = parseInt(decyseconds.toString().substr(0,1), 10);
            }
        } else {
            seconds = parseInt(entry[1], 10);
        }
        minutes = isNaN(minutes) ? 0 : minutes;
        seconds = isNaN(seconds) ? 0 : seconds;
        decyseconds = isNaN(decyseconds) ? 0 : decyseconds;

        return ((minutes * 60 + seconds) * presenter.fps) + decyseconds;
    };

    presenter.timeEntry = function AddonTextAudio_timeEntry (slide_time, fromSrtFile) {
        var entry = slide_time.split('-');
        if (entry.length != 2) {
            return {
                errorCode: 'M03',
                errorData: slide_time
            }
        }

        if (fromSrtFile) {
            return {
                start: Math.round(SrtParser.timeColonSeparatedToMs(entry[0]) / 100),
                end: Math.round(SrtParser.timeColonSeparatedToMs(entry[1]) / 100)
            };
        }

        return {
            start: presenter.toFrames(entry[0]),
            end: presenter.toFrames(entry[1])
        };
    };

    presenter.createNewSpan = function addonTextAudio_createNewSpan(n1, n2, text) {
        var $span = $('&lt;span&gt;&lt;/span&gt;').
            addClass('textelement' + n1).
            attr('data-selectionid', n1).
            attr('data-intervalid', n2).
            html(text);
        return $span.prop("outerHTML");
    };

    presenter.parseSlideText = function addonTextAudio_parseSlideText (text) {
        var elemNumber = 0;
        var resultHTML = "";

        HTMLParser(text, {
            start: function (tag, attrs, unary) {
                resultHTML += "&lt;" + tag;
                for (var i=0; i&lt;attrs.length; i++) {
                    resultHTML += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
                }
                resultHTML += (unary ? "/" : "") + "&gt;";
            },
            end: function (tag) {
                resultHTML += "&lt;/" + tag + "&gt;";
            },
            chars: function (text) {
                var localParts = text.split('||');

                for (var i=0; i&lt;localParts.length; i++) {
                    if (localParts[i] !== '') {
                        resultHTML += presenter.createNewSpan(elemNumber, presenter.globalIntervalNumber, localParts[i]);
                    }

                    if (i !== localParts.length-1) {
                        elemNumber++;
                        presenter.globalIntervalNumber++;
                    }
                }
            }
        });

        return resultHTML;
    };

    presenter.handleSlides = function AddonTextAudio_handleSlides (slides) {
        const newSlides = [];

        for (var i=0; i&lt;slides.length; i++) {
            let slide = $.extend({}, slides[i]);
            if (!slide.fileSRT) {
                slide.fromFile = false;
                newSlides.push(slide);
                continue;
            }

            let response = presenter.downloadSRTSync(slide.fileSRT);
            if (response.error) {
                console.error("Error while downloading SRT file. Response code is: " + response.responseCode);
                return {
                    isValid: false,
                    errorCode: 'M06'
                };
            }

            let parsed = SrtParser.parse(response.text, true);
            let cropTime = SrtParser.timeColonSeparatedToMs(slide.cropTime);
            if (cropTime === 0 &amp;&amp; slide.cropTime.trim()) {
                return {
                    isValid: false,
                    errorCode: 'M07'
                };
            }
            if (cropTime !== 0) {
                parsed = SrtParser.cropTimes(parsed, cropTime);
            }
            let formattedData = presenter.formatData(parsed);
            slide.Text = formattedData.text;
            slide.Times = formattedData.time;
            slide.fromFile = true;
            newSlides.push(slide);
        }

        return presenter.validateSlides(newSlides);
    };

    presenter.formatData = function AddonTextAudio_formatData (data) {
        let text = "";
        let time = "";

        data.forEach((d) =&gt; {
           text += d.text + "||&lt;br&gt;";
           time += `${SrtParser.msToTimeColonSeparated(d.startTime)}-${SrtParser.msToTimeColonSeparated(d.endTime)}\n`
        });

        text = text.replace(/\|\|&lt;br&gt;$/gm, "");
        time = time.replace(/\n$/gm, "");
        return {text, time};
    };

    presenter.validateSlides = function AddonTextAudio_validateSlides (slides) {
        var validationResult = {
            isValid: false,
            value: [{
                Text: [''],
                Times: [{start: 0, end: 0}],
                PosAndDim: ['']
            }],
            errorCode: false
        };
        var frames = [],
            interval = 0;
        for (var i=0; i&lt;slides.length; i++) {
            var slide = slides[i];
            var slide_texts = slide.Text.split('||');
            var parsed_slide_texts = presenter.parseSlideText(slide.Text);
            var slide_times = slide.Times.split('\n');
            var slide_intervals = [];
            var posAndDims;

            if(slide.positionAndDimentions != '' &amp;&amp; slide.positionAndDimentions != undefined){
                posAndDims = slide.positionAndDimentions.split(';');
            }else{
                posAndDims = '';
            }

            if (slide_texts.length != slide_times.length) {
                validationResult.errorCode = 'M02';
                return validationResult;
            }

            for (var j=0; j&lt;slide_times.length; j++) {
                var entry = slide_times[j];
                slide_times[j] = presenter.timeEntry(entry, slide.fromFile);
                if (slide_times[j].errorCode) {
                    validationResult.errorCode = slide_times[j].errorCode;
                    validationResult.errorData = entry;
                    return validationResult;
                }

                var entry_start = slide_times[j].start,
                    entry_end = slide_times[j].end;

                if (entry_start &gt; entry_end) {
                    validationResult.errorCode = 'M04';
                    return validationResult;
                }
                if (frames.length &gt; Math.round(entry_start)) {
                    validationResult.errorData = entry_start;
                    validationResult.errorCode = 'M05';
                    return validationResult;
                }

                var frame;
                for (frame=frames.length; frame &lt; entry_start; frame++) {
                    frames[frame] = {
                        slide_id: -1,
                        selection_id: -1
                    }
                }
                for (frame=entry_start; frame &lt; entry_end; frame++) {
                    frames[frame] = {
                        slide_id: i,
                        selection_id: j
                    }
                }
                slide_intervals.push(interval);
                interval++;
            }

            slide.Text = slide_texts;
            slide.Times= slide_times;
            slide.intervals = slide_intervals;
            slide.html = parsed_slide_texts;
            slides[i] = slide;
            slide.positionAndDimentions = posAndDims;
        }
        validationResult.isValid = true;
        validationResult.value = slides;
        validationResult.frames = frames;

        presenter.slidesLengths = [];
        presenter.totalNumberOfParts = interval;

        return validationResult;
    };

    presenter.validateSeparateFiles = function addonTextAudio_validateSeparateFiles (audioFiles) {
        function addonTextAudio_hasEmptyField (audioFiles) {
            return audioFiles.filter(function AddonTextAudio_audioFilesFilter (v) {
                return ModelValidationUtils.isStringEmpty(v.mp3) || ModelValidationUtils.isStringEmpty(v.ogg);
            }).length &gt; 0;
        }

        if (ModelValidationUtils.isArrayEmpty(audioFiles)) {
            return presenter.getErrorObject('SAF01');
        }

        if (presenter.totalNumberOfParts !== audioFiles.length) {
            return presenter.getErrorObject('SAF02');
        }

        if (addonTextAudio_hasEmptyField(audioFiles)) {
            return presenter.getErrorObject('SAF03');
        }

        return presenter.getCorrectObject(audioFiles);
    };

    presenter.validateVocabularyIntervals = function addonTextAudio_validateVocabularyIntervals (intervals) {
        var returnObj = {
            intervals: undefined,
            errorCode: false
        };

        if (intervals === undefined) {
            returnObj.errorCode = 'VI03';
            return returnObj;
        }

        var vocIntervals = intervals.split('\n');
            intervals = [];
        if (vocIntervals.length != presenter.totalNumberOfParts) {
            returnObj.errorCode = 'VI02';
            return returnObj;
        }

        for (var i=0; i&lt;vocIntervals.length; i++) {
            intervals.push(presenter.timeEntry(vocIntervals[i]));
        }

        returnObj.intervals = intervals;
        return returnObj;
    };

    presenter.downloadSRTSync = function AddonTextAudio_downloadSRTSync (src) {
        const res = {
            error: false,
            responseCode: 200,
            text: ""
        };

        const req = new XMLHttpRequest();
        req.open("GET", src, false);
        req.send();

        if (req.status === 200) {
            res.text = req.responseText;
        } else {
            res.error = true;
            res.responseCode = req.status;
        }

        return res;
    };

    presenter.validateModel = function addonTextAudio_validateModel (model) {
        if (model.clickAction === '') {
            model.clickAction = presenter.ALLOWED_CLICK_BEHAVIOUR.play_from_the_moment;
        }
        var validatedAudioFiles = null,
            transposedBehaviors = presenter.transposeDict(presenter.ALLOWED_CLICK_BEHAVIOUR),
            clickAction = transposedBehaviors[model.clickAction];
        presenter.originalFile .mp3 = model.mp3;
        presenter.originalFile .ogg = model.ogg;

        if (!presenter.originalFile .ogg &amp;&amp; !presenter.originalFile .mp3) {
            return presenter.getErrorObject('M01');
        }

        presenter.totalNumberOfParts = 0;
        var validatedSlides = presenter.handleSlides(model.Slides);
        var validatedVocabularyIntervals = presenter.validateVocabularyIntervals(model.vocabulary_intervals);
        if (validatedSlides.errorCode) {
            return presenter.getErrorObject(validatedSlides.errorCode);
        }

        if (clickAction == 'play_vocabulary_file' || clickAction == 'play_interval_or_vocabulary') {
            validatedAudioFiles = presenter.validateSeparateFiles(model.separateFiles);
            if (!validatedAudioFiles.isValid) return presenter.getErrorObject(validatedAudioFiles.errorCode);
        } else {
            validatedAudioFiles = presenter.getCorrectObject(false);
        }

        if (clickAction == 'play_vocabulary_interval') {
            presenter.vocabularyFile.mp3 = model.vocabulary_mp3;
            presenter.vocabularyFile.ogg = model.vocabulary_ogg;
            if (!presenter.vocabularyFile.mp3 &amp;&amp; !presenter.vocabularyFile.ogg) {
                return presenter.getErrorObject('VI01');
            }
            if (validatedVocabularyIntervals.errorCode) {
                return presenter.getErrorObject(validatedVocabularyIntervals.errorCode);
            }
        }

        if(model.showSlides == undefined){
            model.showSlides = "Show current slide";
        }

        return {
            isValid: true,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            onEndEventCode: model.onEnd,
            enableLoop: ModelValidationUtils.validateBoolean(model.enableLoop),
            displayTime: ModelValidationUtils.validateBoolean(model.displayTime),
            controls: model.controls,
            slides: validatedSlides.value,
            frames: validatedSlides.frames,
            clickAction: clickAction,
            playPart: (clickAction == 'play_interval' || clickAction == 'play_interval_or_vocabulary'),
            separateFiles: validatedAudioFiles.value,
            playSeparateFiles: (clickAction == 'play_vocabulary_file' || clickAction == 'play_interval_or_vocabulary'),
            vocabulary_mp3: model.vocabulary_mp3,
            vocabulary_ogg: model.vocabulary_ogg,
            vocabularyIntervals: validatedVocabularyIntervals.intervals,
            isClickDisabled: ModelValidationUtils.validateBoolean(model.isClickDisabled),
            showSlides: model.showSlides,
            isEnabled: !ModelValidationUtils.validateBoolean(model["isDisabled"]),
            langAttribute: model.langAttribute,
            enablePlaybackSpeedControls: ModelValidationUtils.validateBoolean(model.enablePlaybackSpeedControls)
        };
    };

    presenter.executeCommand = function addonTextAudio_executeCommand (name, params) {
        var commands = {
            'play': presenter.play,
            'stop': presenter.stop,
            'pause': presenter.pause,
            'show': presenter.show,
            'hide': presenter.hide,
            'enable': presenter.enable,
            'disable': presenter.disable,
            'setPlaybackRate': presenter.setPlaybackRate
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function AddonTextAudio_setVisibility (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.pauseZero = function addonTextAudio_pauseZero () {
        presenter.stopTimeMeasurement();

        if (!presenter.audio.paused &amp;&amp; presenter.isLoaded) {
            presenter.stopClicked = true;
            presenter.audio.pause();
            presenter.audio.currentTime = 0;
        }
    };

    presenter.setPlaybackRate = function (value) {
        var parsedValue = parseFloat(value);
        presenter.playbackRate = parsedValue;
        presenter.audio.playbackRate = parsedValue;
        displayPlaybackOptions();
    };

    presenter.changePlaybackRate = function (type) {
        var index = presenter.PLAYBACK_RATE_LIST.indexOf(presenter.playbackRate);
        var $select = presenter.$playbackRateControls.find('select');

        switch (type) {
            case presenter.OPERATION_TYPE.INCREASE:
                if (index === (presenter.PLAYBACK_RATE_LIST.length - 1)) return;
                index++;
                break;
            case presenter.OPERATION_TYPE.DECREASE:
                if (index === 0) return;
                index--;
                break;
        }

        $select.val(presenter.PLAYBACK_RATE_LIST[index]);
        presenter.playbackRate = presenter.PLAYBACK_RATE_LIST[index];
        presenter.audio.playbackRate = presenter.playbackRate;
    };

    presenter.play = isModuleEnabledDecorator(true)(function addonTextAudio_play () {
        presenter.startTimeMeasurement();
        if (presenter.audio.paused) {
            presenter.stopClicked = false;
            presenter.pauseZero();
            presenter.audio.play();
            if (presenter.configuration.controls === controls.CUSTOM) {
                presenter.$playPauseBtn.
                    removeClass(presenter.CSS_CLASSES.PLAY_BUTTON).
                    addClass(presenter.CSS_CLASSES.PAUSE_BUTTON);
            }
        }
    });

    function forceStop () {
        if (presenter.configuration.controls === controls.CUSTOM &amp;&amp; presenter.isLoaded) {
            presenter.stopClicked = true;
            presenter.$playPauseBtn.
                addClass(presenter.CSS_CLASSES.PLAY_BUTTON).
                removeClass(presenter.CSS_CLASSES.PAUSE_BUTTON);
            if (presenter.audio.paused) {
                presenter.stopClicked = true;
                presenter.audio.currentTime = 0;
            }
        }
        presenter.removeMarkFromItems();
        presenter.pauseZero();
        if (!presenter.isLoaded) {
            presenter.audio.addEventListener("loadeddata", presenter.stopAudioLoadedData);
        }
    }

    presenter.stop = isModuleEnabledDecorator(true)(function addonTextAudio_stop () {
        forceStop();
    });

    presenter.stopAudioLoadedData = function AddonTextAudio_StopAudioLoadedData () {
        presenter.isLoaded = true;
        forceStop();
    };

    presenter.playPartStop = function addonTextAudio_playPartStop () {
        presenter.pauseZero();
        if (!presenter.isLoaded) {
            presenter.audio.addEventListener("loadeddata", presenter.playAudioLoadedData);
        }
    };

    presenter.playAudioLoadedData = function AddonTextAudio_playAudioLoadedData () {
        presenter.isLoaded = true;
        presenter.stop();
    };

    presenter.pause = isModuleEnabledDecorator(true)(function addonTextAudio_pause () {
        presenter.stopTimeMeasurement();

        if (!presenter.audio.paused) {
            presenter.audio.pause();
            if (presenter.configuration.controls === controls.CUSTOM) {
                presenter.$playPauseBtn.
                    removeClass(presenter.CSS_CLASSES.PAUSE_BUTTON).
                    addClass(presenter.CSS_CLASSES.PLAY_BUTTON);
            }
        }
    });

    presenter.show = function addonTextAudio_show () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hideAddon = function addonTextAudio_hideAddon () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.hide = function addonTextAudio_hide () {
        presenter.stop();
        presenter.hideAddon();
    };

    presenter.isEnabledInGSAMode = function () {
        return true
    }

    presenter.reset = function addonTextAudio_reset () {
        presenter.disabledTime = 0;
        if (presenter.isEnabledByDefault) {
            presenter.enable();
        } else {
            presenter.disable();
        }

        forceStop();

        presenter.hasBeenStarted = false;
        presenter.isPlaying = false;
        presenter.playedByClick = false;

        presenter.configuration.isVisible = presenter.isVisibleByDefault;
        if (presenter.configuration.isVisible) {
            presenter.show();
        } else {
            presenter.hideAddon();
        }
    };

    presenter.enable = isModuleEnabledDecorator(false)(function () {
        presenter.configuration.isEnabled = true;
        presenter.$view.removeClass('disabled');

        if (presenter.configuration.controls === controls.BROWSER) {
            presenter.setAudioSrc();
            presenter.audio.currentTime = presenter.disabledTime;
        }
    });

    presenter.disable = isModuleEnabledDecorator(true)(function () {
        presenter.pause();
        presenter.configuration.isEnabled = false;
        presenter.$view.addClass('disabled');

        if (presenter.configuration.controls === controls.BROWSER) {
            presenter.disabledTime = presenter.audio.currentTime;
            presenter.audio.removeAttribute('src');
            presenter.audio.load();
        }
    });

    presenter.clearSelection = function addonTextAudio_clearSelection () {
        presenter.$view.find('.textaudio-text span.active').removeClass('active');
    };

    presenter.getState = function addonTextAudio_getState () {
        return JSON.stringify({
            isVisible : presenter.configuration.isVisible,
            isEnabled: presenter.configuration.isEnabled
        });
    };

    presenter.setState = function addonTextAudio_setState (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return false;
        if (!presenter.configuration.isValid) return false;

        presenter.stop();

        var data = JSON.parse(stateString);

        if (data.isVisible) {
            presenter.show();
        } else {
            presenter.hideAddon();
        }

        // Fix due to a bug that put wrong state. If the value in model is undefined, than module is enabled
        if (presenter.originalIsDisabledModel === undefined) {
            presenter.enable();
        }else if (!data.isEnabled &amp;&amp; data.isEnabled !== undefined) {
            presenter.disable();
        } else {
            presenter.enable();
        }

        return false;
    };

    presenter.executeOnEndEvent = function AddonTextAudio_executeOnEndEvent () {
        if (presenter.configuration.onEndEventCode) {
            presenter.playerController.getCommands().executeEventCode(presenter.configuration.onEndEventCode);
        }
    };

    // For tests purposes.
    presenter.__internalElements = {
        isEnabledDecorator: isModuleEnabledDecorator
    };

    presenter.setWCAGStatus = function (isOn) {
        presenter.isWCAGOn = isOn;
    };

    presenter.getElementsForKeyboardNavigation = function () {
        let elements;
        switch (presenter.configuration.controls) {
            case controls.CUSTOM:
                elements = [
                    presenter.$playPauseBtn,
                    presenter.$stopBtn
                ];
                elements.push(presenter.$progressWrapper);
                elements.push(presenter.$volumeBtn);
                if (presenter.configuration.enablePlaybackSpeedControls) {
                    elements.push(presenter.$playbackRateControls);
                }
                break;
            default:
                elements = [];
                break;
        }

        return elements.concat(presenter.slidesSpanElements.map(
            function(spanElement) {
                return $(spanElement);
            }
        ));
    };

    presenter.actualizeKeyboardControllerElements = function () {
        if (presenter.isVocabularyOrProgressActive()) {
            return;
        }

        if (presenter.keyboardControllerObject) {
            const elements = presenter.getElementsForKeyboardNavigation();
            presenter.keyboardControllerObject.setElements(elements);
        }
    };

    presenter.isVocabularyOrProgressActive = function () {
        const keyboardController = presenter.keyboardControllerObject;
        if (!keyboardController) return false;

        const currentElement = keyboardController.getCurrentElement();

        return keyboardController.isVolumeControlActive(currentElement) || keyboardController.isProgressBarActive(currentElement);
    }

    presenter.buildKeyboardController = function () {
        const elements = presenter.getElementsForKeyboardNavigation();
        presenter.keyboardControllerObject = new TextAudioKeyboardController(elements, 1);
    }

    presenter.keyboardController = function (keycode, isShiftDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftDown, event);
    };

    function TextAudioKeyboardController(elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
        this.updateMapping();

        // The 'selectedMode' variable is used to determine if a space has already been used on the current element.
        // When the navigation element is changed this variable should take the value false.
        this.selectedMode = false;
    }

    TextAudioKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    TextAudioKeyboardController.prototype.constructor = TextAudioKeyboardController;

    TextAudioKeyboardController.prototype.updateMapping = function () {
        this.mapping[window.KeyboardControllerKeys.ARROW_UP] = this.up;
        this.mapping[window.KeyboardControllerKeys.ARROW_DOWN] = this.down;
    };

    TextAudioKeyboardController.prototype.setElements = function (elements) {
        KeyboardController.prototype.setElements.call(this, elements);
        if (this.keyboardNavigationActive) {
            this.switchWithoutReadingToFirstVisibleElement();
        }
    };

    TextAudioKeyboardController.prototype.getCurrentElement = function () {
        return $(this.getTarget(this.keyboardNavigationCurrentElement, false));
    };

    TextAudioKeyboardController.prototype.mark = function (element) {
        KeyboardController.prototype.mark.call(this, element);

        const $currentElement = this.getCurrentElement();
        if ($currentElement.is('[class^="textelement"]') &amp;&amp; this.isElementVisible($currentElement)) {
            this.scrollVertically($currentElement);
        }
    };

    TextAudioKeyboardController.prototype.scrollVertically = function ($textElement) {
        let pos = $textElement.position().top;
        const $slideWithElement = $textElement.closest(`.textaudio-text`);
        const currentScroll = $slideWithElement.scrollTop();
        const divHeight =  $slideWithElement.height();

        pos = (pos + currentScroll) - (divHeight / 2);

        $slideWithElement.scrollTop(pos);
    }

    TextAudioKeyboardController.prototype.enter = function (event) {
        if (event) {
            event.preventDefault();
        }

        if (!this.keyboardNavigationActive) {
            this.keyboardNavigationActive = true;
            this.switchWithoutReadingToFirstVisibleElement();
        }

        if (presenter.isWCAGOn) {
            this.speakCurrentElement();
        }
    };

    TextAudioKeyboardController.prototype.select = function (event) {
        if (presenter.configuration.isEnabled) {
            this.selectedMode = true;
        }

        const $currentElement = this.getCurrentElement();
        handleVolumeControl($currentElement);
        handleAudioProgress($currentElement);

        if (!presenter.isVocabularyOrProgressActive()) {
            KeyboardController.prototype.select.call(this, event);
        }
    };

    function addActiveClass($element) {
        $element.addClass(presenter.CSS_CLASSES.ACTIVE);
    }

    function handleVolumeControl($element) {
        if (!$element.hasClass(presenter.CSS_CLASSES.VOLUME_BTN)) {
            return;
        }

        addActiveClass($element);
        presenter.$volumeLayer.show();
        presenter.$playerTime.hide();
        presenter.onVolumeChanged();
    }

    function handleAudioProgress($element) {
        if (!$element.hasClass(presenter.CSS_CLASSES.PROGRESS_BAR)) {
            return;
        }

        addActiveClass($element);
    }

    function hideVolumeController() {
        const $element = $(`.${presenter.CSS_CLASSES.VOLUME_BTN}`);
        if ($element &amp;&amp; $element.hasClass(presenter.CSS_CLASSES.ACTIVE)) {
            presenter.$volumeLayer.hide();
            presenter.$playerTime.show();
            $element.removeClass(presenter.CSS_CLASSES.ACTIVE);
        }
    }

    function cleanProgressBar() {
        presenter.$progressWrapper?.removeClass(presenter.CSS_CLASSES.ACTIVE);
    }

    TextAudioKeyboardController.prototype.up = function (event) {
        if (event) {
            event.preventDefault();
        }

        const $currentElement = this.getCurrentElement();
        if (this.isElementActiveForAfterSelectionAction($currentElement)) {
            presenter.changePlaybackRate(presenter.OPERATION_TYPE.INCREASE);
        } else if (this.isVolumeControlActive($currentElement)) {
            presenter.handleVolumeChange(event);
        } else if (this.isProgressBarActive($currentElement)) {
            presenter.handleProgressChange(event);
        } else {
            this.previousRow(event);
        }
    };

    presenter.handleProgressChange = function (event) {
        const currentTime = presenter.audio.currentTime;
        if (event.keyCode === window.KeyboardControllerKeys.ARROW_UP) {
            if (presenter.audio.currentTime !== 0 &amp;&amp; currentTime + presenter.progressStep &lt; presenter.audio.duration) {
                presenter.audio.currentTime += presenter.progressStep;
            } else if (presenter.audio.currentTime === 0) {
                presenter.audio.currentTime = presenter.progressStep;
            } else if (currentTime + presenter.progressStep &gt;= presenter.audio.duration) {
                presenter.audio.currentTime = presenter.audio.duration;
            }
        } else if (event.keyCode === window.KeyboardControllerKeys.ARROW_DOWN) {
            if (currentTime - presenter.progressStep &gt; 0) {
                presenter.audio.currentTime -= presenter.progressStep;
            } else if (currentTime - presenter.progressStep &lt;= 0) {
                presenter.audio.currentTime = 0;
            }
        }
    };

    presenter.handleVolumeChange = function (event) {
        const currentValue = presenter.audio.volume;
        if (event.keyCode === window.KeyboardControllerKeys.ARROW_UP) {
            if (currentValue !== 0 &amp;&amp; currentValue + presenter.volumeStep &lt; 1) {
                presenter.audio.volume += presenter.volumeStep;
            } else if (currentValue === 0) {
                presenter.audio.volume = 0.1;
            } else if (currentValue + presenter.volumeStep &gt;= 1) {
                presenter.audio.volume = 1;
            }
        } else if (event.keyCode === window.KeyboardControllerKeys.ARROW_DOWN) {
            if (currentValue - presenter.volumeStep &gt; 0) {
                presenter.audio.volume -= presenter.volumeStep;
            } else if (currentValue - presenter.volumeStep &lt;= 0) {
                presenter.audio.volume = 0;
            }
        }
    }

    TextAudioKeyboardController.prototype.down = function (event) {
        if (event) {
            event.preventDefault();
        }

        const $currentElement = this.getCurrentElement();
        if (this.isElementActiveForAfterSelectionAction($currentElement)) {
            presenter.changePlaybackRate(presenter.OPERATION_TYPE.DECREASE);
        } else if (this.isVolumeControlActive($currentElement)) {
            presenter.handleVolumeChange(event);
        } else if (this.isProgressBarActive($currentElement)) {
            presenter.handleProgressChange(event);
        } else {
            this.nextRow(event);
        }
    };

    TextAudioKeyboardController.prototype.isElementActiveForAfterSelectionAction = function ($element) {
        return (presenter.configuration.isEnabled
            &amp;&amp; this.selectedMode
            &amp;&amp; $element.hasClass(presenter.CSS_CLASSES.AUDIO_SPEED_CONTROLLER)
        );
    };

    TextAudioKeyboardController.prototype.isVolumeControlActive = function ($element) {
        return $element.hasClass(presenter.CSS_CLASSES.VOLUME_BTN) &amp;&amp;
            $element.hasClass(presenter.CSS_CLASSES.ACTIVE);
    };

    TextAudioKeyboardController.prototype.isProgressBarActive = function ($element) {
        return $element.hasClass(presenter.CSS_CLASSES.PROGRESS_BAR) &amp;&amp;
            $element.hasClass(presenter.CSS_CLASSES.ACTIVE);
    };

    TextAudioKeyboardController.prototype.nextElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(1);

        if (!this.isCurrentElementVisible() &amp;&amp; !this.isLastVisibleElementEqualsCurrentElement()) {
            this.nextElement(event);
        }
    };

    TextAudioKeyboardController.prototype.previousElement = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(-1);

        if (!this.isCurrentElementVisible() &amp;&amp; !this.isLastVisibleElementEqualsCurrentElement()) {
            this.previousElement(event);
        }
    };

    TextAudioKeyboardController.prototype.nextRow = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(this.columnsCount);

        if (!this.isCurrentElementVisible() &amp;&amp; !this.isLastVisibleElementEqualsCurrentElement()) {
            this.nextRow(event);
        }
    };

    TextAudioKeyboardController.prototype.previousRow = function (event) {
        if (event) {
            event.preventDefault();
        }
        this.switchElement(-this.columnsCount);

        if (!this.isCurrentElementVisible() &amp;&amp; !this.isLastVisibleElementEqualsCurrentElement()) {
            this.previousRow(event);
        }
    };

    TextAudioKeyboardController.prototype.switchWithoutReadingToFirstVisibleElement = function () {
        for (let i = 0; i &lt; this.keyboardNavigationElementsLen; i++) {
            let $nextElement = this.keyboardNavigationElements[i];
            if (this.isElementVisible($nextElement)) {
                this.lastVisibleElementIndex = i;
                this.keyboardNavigationCurrentElementIndex = i;
                this.keyboardNavigationCurrentElement = $nextElement;
                this.mark($nextElement);
                return;
            }
        }
    };

    TextAudioKeyboardController.prototype.isCurrentElementVisible = function () {
        return this.isElementVisible(this.getCurrentElement());
    };

    TextAudioKeyboardController.prototype.isElementVisible = function ($element) {
        const style = window.getComputedStyle($element[0]);
        return style.display !== 'none' &amp;&amp; style.visibility === 'visible';
    };

    TextAudioKeyboardController.prototype.isLastVisibleElementEqualsCurrentElement = function () {
        return this.lastVisibleElementIndex === this.keyboardNavigationCurrentElementIndex;
    };

    TextAudioKeyboardController.prototype.switchElement = function (move) {
        hideVolumeController();
        cleanProgressBar();

        this.selectedMode = false;
        KeyboardController.prototype.switchElement.call(this, move);
        if (this.isCurrentElementVisible()) {
            this.lastVisibleElementIndex = this.keyboardNavigationCurrentElementIndex;
            if (presenter.isWCAGOn) {
                this.speakCurrentElement();
            }
        }
    };

    TextAudioKeyboardController.prototype.escape = function (event) {
        presenter.pause();

        if (!presenter.audio) {
            presenter.audio.currentTime = 0;
        }

        hideVolumeController();
        cleanProgressBar();

        KeyboardController.prototype.escape.call(this, event);
    };

    TextAudioKeyboardController.prototype.exitWCAGMode = function () {
        this.selectedMode = false;
        KeyboardController.prototype.exitWCAGMode.call(this);

        hideVolumeController();
        cleanProgressBar();
    };

    TextAudioKeyboardController.prototype.speakCurrentElement = function () {
        const $currentElement = this.getCurrentElement();

        if ($currentElement.hasClass(presenter.CSS_CLASSES.PLAY_PAUSE_BUTTON)) {
            this.speakForPlayPauseButton();
        } else if ($currentElement.hasClass(presenter.CSS_CLASSES.STOP_BUTTON)) {
            this.speakForStopButton();
        } else if ($currentElement.hasClass(presenter.CSS_CLASSES.AUDIO_SPEED_CONTROLLER)) {
            this.speakForSpeedController();
        } else if ($currentElement.hasClass(presenter.CSS_CLASSES.PROGRESS_BAR)) {
            this.speakForProgressBar();
        } else if ($currentElement.hasClass(presenter.CSS_CLASSES.VOLUME_BTN)) {
            this.speakForVolumeController();
        }
    };

    TextAudioKeyboardController.prototype.speakForPlayPauseButton = function () {
        const textToSpeak = presenter.isPlaying
            ? presenter.speechTexts.Pause
            : presenter.speechTexts.Play;
        speakMessage(textToSpeak);
    };

    TextAudioKeyboardController.prototype.speakForStopButton = function () {
        speakMessage(presenter.speechTexts.Stop);
    };

    TextAudioKeyboardController.prototype.speakForSpeedController = function () {
        speakMessage(presenter.speechTexts.AudioSpeedController);
    };

    TextAudioKeyboardController.prototype.speakForProgressBar = function () {
        speakMessage(presenter.speechTexts.ProgressBar);
    };

    TextAudioKeyboardController.prototype.speakForVolumeController = function () {
        speakMessage(presenter.speechTexts.VolumeController);
    };

    function speakMessage(message) {
        const speechTextVoices = [TTSUtils.getTextVoiceObject(message)];
        presenter.speak(speechTextVoices);
    }

    presenter.speak = function (data) {
        const tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function AddonTextAudio_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Text_Coloring" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property displayName="Colors" name="colors" nameLabel="Text_Coloring_property_colors" type="list">
			<property displayName="Color ID" name="id" nameLabel="Text_Coloring_property_color_id" type="string"/>
			<property displayName="Color" name="color" nameLabel="Text_Coloring_property_color" type="string"/>
			<property displayName="Description" isLocalized="true" name="description" nameLabel="Text_Coloring_property_description" type="string"/>
		</property>
		<property displayName="Text" isDefault="true" isLocalized="true" name="text" nameLabel="Text_Coloring_property_text" type="text"/>
		<property displayName="Is Not Activity" name="isNotActivity" nameLabel="Text_Coloring_property_is_not_activity" type="boolean"/>
		<property displayName="Buttons position" name="buttonsPosition" nameLabel="Text_Coloring_property_buttons_position" type="{left, top, bottom, right}"/>
		<property displayName="Hide Color Buttons" name="hideColorsButtons" nameLabel="Text_Coloring_property_hide_color_buttons" type="boolean"/>
		<property displayName="Show all answers in gradual show answers mode" name="showAllAnswersInGradualShowAnswersMode" nameLabel="Text_Coloring_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
		<property displayName="Show set eraser mode button" name="showSetEraserModeButton" nameLabel="Text_Coloring_property_show_set_eraser_mode_button" type="boolean"/>
		<property displayName="Eraser button text" isLocalized="true" name="eraserButtonText" nameLabel="Text_Coloring_property_eraser_button_text" type="string"/>
		<property name="Mode" nameLabel="Text_Selection_property_mode" type="{All selectable, Mark phrases to select}"/>
		<property displayName="Count Errors" name="countErrors" nameLabel="Text_Coloring_property_count_errors" type="boolean"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
		<property name="Legend title" nameLabel="legend_title" type="string"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="Text_Coloring_property_speech_texts" type="staticlist">
			<property name="Selected" nameLabel="Text_Coloring_property_selected" type="staticrow">
				<property name="Selected" nameLabel="Text_Coloring_property_selected" type="string"/>
			</property>
			<property name="Deselected" nameLabel="Text_Coloring_property_deselected" type="staticrow">
				<property name="Deselected" nameLabel="Text_Coloring_property_deselected" type="string"/>
			</property>
			<property name="TextContent" nameLabel="Text_Coloring_property_TextContent" type="staticrow">
				<property name="TextContent" nameLabel="Text_Coloring_property_TextContent" type="string"/>
			</property>
			<property name="Correct" nameLabel="Text_Coloring_property_correct" type="staticrow">
				<property name="Correct" nameLabel="Text_Coloring_property_correct" type="string"/>
			</property>
			<property name="Incorrect" nameLabel="Text_Coloring_property_Incorrect" type="staticrow">
				<property name="Incorrect" nameLabel="Text_Coloring_property_Incorrect" type="string"/>
			</property>
		</property>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Text_Coloring_property_lang_attribute" type="string"/>
	</model>
<css>.text-coloring-hover-class {
	background-color: orange;
}

.text-coloring-pointer-class {
	cursor: pointer;
}

.text-coloring-active-button {
	background-color: orange;
}

/*TOP POSITION*/
.text-coloring-eraser-button-container-top-position {
	display: inline-block;
	margin-right: 20px;
}

.text-coloring-colors-buttons-container-top-position {
	display: inline-block;
}

.text-coloring-colors-buttons-container-top-position &gt; .text-coloring-color-button {
	display: inline-block;
	margin-right: 10px;
}

.text-coloring-buttons-container-top-position {
	margin-bottom: 10px;
}

/*LEFT POSITION*/
.text-coloring-main-container-left-position {
	display: table-row;
}

.text-coloring-buttons-container-left-position {
	display: table-cell;
}

.text-coloring-tokens-container-left-position {
	display: table-cell;
}

.text-coloring-eraser-button-container-left-position {
	margin-top: 10px;
}

.text-coloring-colors-buttons-container-left-position &gt; .text-coloring-color-button{
	margin-bottom: 10px;
}
/*BOTTOM POSITION*/
.text-coloring-colors-buttons-container-bottom-position {
	display: inline-block;
}

.text-coloring-colors-buttons-container-bottom-position &gt; .text-coloring-color-button{
	margin-right: 10px;
	display: inline-block;
}

.text-coloring-eraser-button-container-bottom-position {
	display: inline-block;
}

.text-coloring-buttons-container-bottom-position {
	margin-top: 10px;
}

/*RIGHT POSITION*/
.text-coloring-main-container-right-position {
	display: table-row;
}

.text-coloring-tokens-container-right-position {
	display: table-cell;
}

.text-coloring-buttons-container-right-position {
	display: table-cell;
}

.text-coloring-buttons-container-right-position &gt; .text-coloring-color-button {
	margin-bottom: 10px;
}

.text-coloring-eraser-button-container-right-position {
	margin-top: 10px;
}

.text-coloring-token-correct-marking {
	background-color: green;
}

.text-coloring-token-wrong-marking {
	background-color: red;
}
</css><view/><preview/><presenter>/*
 KNOWN ISSUES
    Text parsing:
        Do not change the space special character (&amp;nbsp;) support due to backward compatibility #6613
        Do not add html escape support due to backward compatibility #6902
*/

function AddonText_Coloring_create() {
    var presenter = function () {};
    presenter.printableState = null;
    presenter.initialVisibility = null;
    presenter.isFirstEnter = true;

    function markAsValidValues(value) {
        value.isValid = true;
        return value;
    }

    function filterInvalid(definition) {
        return !definition.isValid;
    }

    function removeIsValidFlag(colorDefinition) {
        delete colorDefinition.isValid;
        return colorDefinition;
    }

    function filterSelectableTokens(token) {
        return token.type == presenter.TOKENS_TYPES.SELECTABLE;
    }

    function filterSelectedTokens(token) {
        return token.isSelected;
    }

    function filterWrongTokens(token) {
        if (token.type === presenter.TOKENS_TYPES.INTRUDER) {
            return token.isSelected;
        }

        if (token.color == undefined) {
            return true;
        }

        return token.selectionColorID != token.color;
    }

    function flattenArrays(result, array) {
        array.forEach(function (element) {
            if (element)
                result.push(element);
        });

        return result;
    }

    presenter.MODE = {
        'Mark phrases to select': 'MARK_PHRASES',
        'All selectable': 'ALL_SELECTABLE',
        DEFAULT: 'All selectable'
    };

    presenter.DEFAULT_TTS_PHRASES = {
        selected: "selected",
        deselected: "deselected",
        textContent: "text content",
        correct: "correct",
        incorrect: "wrong"
    };

    function parseIDs(colorDefinition) {
        var trimmedColorID = colorDefinition.id.trim();
        if (ModelValidationUtils.isStringEmpty(trimmedColorID)) {
            colorDefinition.isValid = false;
            colorDefinition.errorCode = presenter.ERROR_CODES_KEYS.TC_COLORS_COLOR_MUST_HAVE_ID;
            return colorDefinition;
        }

        colorDefinition.id = trimmedColorID;

        return colorDefinition;
    }

    function parseRGBHex(colorDefinition) {
        var parsedColor = ModelValidationUtils.validateColor(colorDefinition.color.trim());

        colorDefinition.color = parsedColor.color;

        if (!parsedColor.isValid) {
            colorDefinition.errorCode = presenter.ERROR_CODES_KEYS.TC_COLORS_COLOR_DEFINITION_HAVE_TO_BE_RGB_HEX;
            colorDefinition.isValid = false;
        }

        return colorDefinition;
    }

    function parseDescriptions(colorDefinition) {
        colorDefinition.description = colorDefinition.description.trim();

        return colorDefinition;
    }

    function getWordToken(value) {
        return {
            "value": value,
            "type": presenter.TOKENS_TYPES.WORD
        };
    }

    function getNewLineToken() {
        return {
            "type": presenter.TOKENS_TYPES.NEW_LINE
        };
    }

    function getSelectableToken(value, colorID) {
        return {
            "type": presenter.TOKENS_TYPES.SELECTABLE,
            "value": value,
            "color": colorID
        };
    }

    function getSpaceToken() {
        return {
            type: presenter.TOKENS_TYPES.SPACE
        };
    }

    function getIntruderToken(value) {
        return {
            "type": presenter.TOKENS_TYPES.INTRUDER,
            "value": value
        };
    }

    function deleteActiveClass() {
        var $buttons = presenter.$view.find(StringUtils.format(".{0}", presenter.defaults.css.activeButton));
        $buttons.removeClass(presenter.defaults.css.activeButton);
    }

    function TextColoringStateMachine(cssConfiguration) {
        StatefullAddonObject.call(this, cssConfiguration);
        this.blocked = false;
        this.currentShowingAnswerId = 0;
        this.notifyEdit();
        this.previousActiveColorID = null;
        this.previousActiveColor = null;
        this.previousEraserMode = null;
    }

    TextColoringStateMachine.prototype = Object.create(StatefullAddonObject.prototype);
    TextColoringStateMachine.constructor = TextColoringStateMachine;

    TextColoringStateMachine.prototype.onBlock = function () {
        presenter.disconnectHandlers();
        this.blocked = true;
    };

    TextColoringStateMachine.prototype.onUnblock = function () {
        presenter.connectHandlers();
        if (presenter.configuration.activeColorID != null || presenter.configuration.eraserMode != null) {
            presenter.connectWordTokensHandlers();
        }
        this.blocked = false;
    };

    TextColoringStateMachine.prototype.onShowAnswers = function () {
        this.onBlockAnswers();
        presenter.configuration.filteredTokens.filter(filterSelectableTokens).forEach(function (token) {
            var colorDefinition = presenter.getColorDefinitionById(token.color);
            if (colorDefinition !== undefined) {
                var $tokenElement = presenter.getWordTokenByIndex(token.index);
                presenter.addShowAnswerClass($tokenElement, colorDefinition.id);
                presenter.markToken($tokenElement, colorDefinition.color);
            }
        });
    };

    TextColoringStateMachine.prototype.onShowSingleAnswer = function () {
        if (!presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            var currentShowingAnswerId = this.currentShowingAnswerId;
            var id = 0;

            if (!this.blocked) {
                this.savePreviousState();
                this.onBlock();
            }

            try {
                presenter.configuration.filteredTokens.filter(filterSelectableTokens).forEach(function (token) {
                    var colorDefinition = presenter.getColorDefinitionById(token.color);
                    if (colorDefinition !== undefined) {
                        if (id === currentShowingAnswerId) {
                            var $tokenElement = presenter.getWordTokenByIndex(token.index);
                            presenter.addShowAnswerClass($tokenElement, colorDefinition.id);
                            presenter.markToken($tokenElement, colorDefinition.color);
                            throw BreakException;
                        }
                        id++;
                    }
                });
            } catch (e) {}

            this.currentShowingAnswerId++;
        } else {
            this.onShowAnswers();
        }
    };

    TextColoringStateMachine.prototype.onBlockAnswers = function () {
        if (!this.blocked) {
            this.savePreviousState();
            this.onBlock();
            presenter.unmarkToken(presenter.$wordTokens);
            presenter.hideTokenClasses(presenter.$wordTokens);
        }
    };

    presenter.addShowAnswerClass = function ($element, colorName) {
        var className = StringUtils.format(presenter.defaults.css.showAnswer, colorName);
        $element.addClass(className);
    };

    presenter.removeShowAnswerClass = function ($element, colorName) {
        var className = StringUtils.format(presenter.defaults.css.showAnswer, colorName);
        $element.removeClass(className);
    };

    TextColoringStateMachine.prototype.onHideAnswers = function () {
        this.currentShowingAnswerId = 0;
        this.blocked = false;
        this.restorePreviousState();
        this.onUnblock();
        presenter.unmarkToken(presenter.$wordTokens);

        presenter.configuration.filteredTokens.filter(filterSelectableTokens).forEach(function (token) {
            var $tokenElement = presenter.getWordTokenByIndex(token.index);
            presenter.removeShowAnswerClass($tokenElement, token.color);
        });

        presenter.configuration.filteredTokens.filter(function (token) {
            return token.isSelected == true;
        }).forEach(function (token) {
            var colorDefinition = presenter.getColorDefinitionById(token.selectionColorID);
            var $tokenElement = presenter.getWordTokenByIndex(token.index);
            presenter.markToken($tokenElement, colorDefinition.color);
        });


        if (this.previousActiveColor != null) {
            presenter.setColorButtonAsActive(this.previousActiveColorID);
        }

        if (this.previousEraserMode) {
            presenter.setEraserButtonAsActive();
        }

        presenter.showTokenClasses(presenter.$wordTokens);
    };

    TextColoringStateMachine.prototype.gradualShowAnswers = function () {
        switch (this._actualState) {
            case StatefullAddonObject._internal.STATE.WORK:
                this.onShowSingleAnswer();
                this.setCssOnShowAnswers();
                break;
            default:
                break;
        }
    };

    TextColoringStateMachine.prototype.gradualBlockAnswers = function () {
        switch (this._actualState) {
            case StatefullAddonObject._internal.STATE.WORK:
                this.onBlockAnswers();
                this.setCssOnShowAnswers();
                break;
            default:
                break;
        }
    };

    TextColoringStateMachine.prototype.gradualHideAnswers = function () {
        switch (this._actualState) {
            case StatefullAddonObject._internal.STATE.WORK:
                StatefullAddonObject._internal.hideAnswersShowAnswersState.call(this);
                break;
            default:
                break;
        }
    };

    TextColoringStateMachine.prototype.isCorrect = function () {
        return true;
    };

    TextColoringStateMachine.prototype.onCorrect = function () {
        this.savePreviousState();
        this.onBlock();
        presenter.configuration.filteredTokens.filter(filterSelectedTokens).forEach(function (token) {
            if (presenter.getScoreForWordMarking(token.index, token.selectionColorID)) {
                presenter.addCorrectClass(token.index);
            } else {
                presenter.addWrongClass(token.index);
            }
        });
    };

    TextColoringStateMachine.prototype.onUnCorrect = function () {
        this.restorePreviousState();
        this.onUnblock();
        presenter.removeAllCheckAnswersClasses();
    };

    TextColoringStateMachine.prototype.savePreviousState = function () {
        this.previousActiveColorID = presenter.configuration.activeColorID;
        this.previousActiveColor = presenter.configuration.activeColor;
        this.previousEraserMode = presenter.configuration.eraserMode;
    };

    TextColoringStateMachine.prototype.restorePreviousState = function () {
        presenter.configuration.activeColorID = this.previousActiveColorID;
        presenter.configuration.activeColor = this.previousActiveColor;
        presenter.configuration.eraserMode = this.previousEraserMode;
    };

    TextColoringStateMachine.prototype.setCssOnCorrect = function () {
        presenter.unsetColorButtonsAsActive();
        presenter.unsetEraserButtonAsActive();
    };

    TextColoringStateMachine.prototype.setCssOnUnCorrect = function () {
        if (this.previousActiveColor != null) {
            presenter.setColorButtonAsActive(this.previousActiveColorID);
        }

        if (this.previousEraserMode) {
            presenter.setEraserButtonAsActive();
        }
    };

    TextColoringStateMachine.prototype.addCssClass = function () {
    };

    TextColoringStateMachine.prototype.removeCssClass = function () {
    };

    TextColoringStateMachine.prototype.onReset = function () {
        presenter.unmarkToken(presenter.$wordTokens);

        presenter.resetColoredWords();

        $.makeArray(presenter.$wordTokens).forEach(function (tokenHTML) {
            presenter.removeColorData($(tokenHTML));
        });

        deleteActiveClass();
        if (presenter.configuration.eraserMode) {
            presenter.toggleEraserMode();
        }

        if (presenter.configuration.activeColorID !== null) {
            presenter.configuration.activeColorID = null;
            presenter.configuration.activeColor = null;
        }
        presenter.disconnectWordTokensHandlers();
        presenter.setColor(presenter.configuration.colors[0].id);
    };

    presenter.defaults = {
        eraserButtonDefaultText: "Eraser Mode",
        dataHolders: {
            colorID: "colorid"
        },
        css: {
            selectableWord: "text-coloring-selectable-word",
            colorButton: "text-coloring-color-button",
            pointer: "text-coloring-pointer-class",
            hover: "text-coloring-hover-class",
            eraserButton: "text-coloring-eraser-button",
            mainContainer: {
                top: 'text-coloring-main-container-top-position',
                left: 'text-coloring-main-container-left-position',
                bottom: 'text-coloring-main-container-bottom-position',
                right: 'text-coloring-main-container-right-position'
            },
            colorButtonsContainer: {
                top: 'text-coloring-colors-buttons-container-top-position',
                left: 'text-coloring-colors-buttons-container-left-position',
                bottom: 'text-coloring-colors-buttons-container-bottom-position',
                right: 'text-coloring-colors-buttons-container-right-position'
            },
            eraserButtonContainer: {
                top: 'text-coloring-eraser-button-container-top-position',
                left: 'text-coloring-eraser-button-container-left-position',
                bottom: 'text-coloring-eraser-button-container-bottom-position',
                right: 'text-coloring-eraser-button-container-right-position'
            },
            buttonsContainer: {
                top: 'text-coloring-buttons-container-top-position',
                left: 'text-coloring-buttons-container-left-position',
                bottom: 'text-coloring-buttons-container-bottom-position',
                right: 'text-coloring-buttons-container-right-position'
            },
            tokensContainer: {
                top: 'text-coloring-tokens-container-top-position',
                left: 'text-coloring-tokens-container-left-position',
                bottom: 'text-coloring-tokens-container-bottom-position',
                right: 'text-coloring-tokens-container-right-position'
            },
            markings: {
                correct: 'text-coloring-token-correct-marking',
                wrong: 'text-coloring-token-wrong-marking'
            },
            activeButton: 'text-coloring-active-button',
            coloredWord: 'text-coloring-colored-with-{0}',
            showAnswer: 'text-coloring-show-answers-{0}'
        }
    };

    presenter.ERROR_CODES = {
        "TC_COLORS_COLOR_DEFINITION_HAVE_TO_BE_RGB_HEX": "Color definitions in colors property have to be proper rgb hex e.g #FF0000 (red)",
        "TC_COLORS_COLOR_MUST_HAVE_ID": "Color definitions in colors property must have id",
        "TC_TEXT_COLOR_DEFINITION_WRONG_ID": "Text Coloring has to use defined color id"
    };

    presenter.ERROR_CODES_KEYS = {
        "TC_COLORS_COLOR_DEFINITION_HAVE_TO_BE_RGB_HEX": "TC_COLORS_COLOR_DEFINITION_HAVE_TO_BE_RGB_HEX",
        "TC_COLORS_COLOR_MUST_HAVE_ID": "TC_COLORS_COLOR_MUST_HAVE_ID",
        "TC_TEXT_COLOR_DEFINITION_WRONG_ID": "TC_TEXT_COLOR_DEFINITION_WRONG_ID"
    };

    presenter.TOKENS_TYPES = {
        WORD: "word",
        SELECTABLE: "selectable",
        NEW_LINE: "new_line",
        SPACE: "space",
        INTRUDER: "intruder"
    };

    presenter.POSITIONS = {
        LEFT: "left",
        RIGHT: "right",
        TOP: "top",
        BOTTOM: "bottom"
    };

    presenter.EVENT_TYPES = {
        SELECTING: 1,
        DESELECTING: 0
    };

    presenter.eventBus = null;
    presenter.view = null;
    presenter.stateMachine = null;
    presenter.$colorButtons = null;
    presenter.$eraserButton = null;
    presenter.$wordTokens = null;

    presenter.configuration = {
        ID: "id",
        isValid: true,
        isError: false,
        textTokens: [],
        filteredTokens: [],
        colors: [],
        buttonsPosition: "left",
        showAllAnswersInGradualShowAnswersMode: false,
        showSetEraserButtonMode: false,
        hideColorsButtons: false,
        activeColor: null,
        activeColorID: null,
        eraserMode: false,
        eraserButtonText: "Eraser Mode",
        isVisible: true
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            presenter.eventBus.addEventListener(events[i], this);
        }
    };


    presenter.run = function (view, model) {
        presenter.runLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.runLogic(view, model, true);
    };

    presenter.destroy = function () {
        presenter.disconnectWordTokensHandlers();
        presenter.disconnectHandlers();
    };

    presenter.runLogic = function (view, model, isPreview) {
        model = presenter.upgradeModel(model);
        presenter.configuration = $.extend({}, presenter.configuration, presenter.validateModel(model));
        presenter.initialVisibility = presenter.configuration.isVisible;
        presenter.view = view;
        presenter.$view = $(view);
        if (presenter.configuration.isError) {
            ModelErrorUtils.showErrorMessage(presenter.view, presenter.ERROR_CODES[presenter.configuration.errorCode]);
            return;
        }

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        presenter.createStateMachine();
        presenter.setFilteredTokensData();
        presenter.setView(presenter.createView());

        if (!presenter.configuration.isVisible &amp;&amp; !isPreview) {
            presenter.hide();
        }

        if (isPreview) {
            presenter.colorTokensInPreview();
        } else {
            presenter.connectHandlers();
            presenter.setColor(presenter.configuration.colors[0].id);
            presenter.stateMachine.notifyEdit();
            presenter.buildKeyboardController();
            presenter.setSpeechTexts(model["speechTexts"]);
        }
    };

    presenter.createStateMachine = function() {
        presenter.stateMachine = new TextColoringStateMachine({});
    };

    presenter.colorTokensInPreview = function () {
        presenter.configuration.filteredTokens.filter(filterSelectableTokens).forEach(function (token) {
            var $token = presenter.getWordTokenByIndex(token.index);
            presenter.markToken($token, token.color);
        });
    };

    presenter.setFilteredTokensData = function () {
        presenter.configuration.filteredTokens = presenter.configuration.textTokens.filter(function (token) {
            return [presenter.TOKENS_TYPES.WORD, presenter.TOKENS_TYPES.SELECTABLE, presenter.TOKENS_TYPES.INTRUDER].includes(token.type);
        }).map(function (token, index) {
            token.isSelected = false;
            token.selectionColorID = null;
            token.index = index;
            return token;
        });
    };

    presenter.setView = function (view) {
        presenter.$view.html(StringUtils.format("&lt;div class='text_coloring'&gt;{0}&lt;/div&gt;", view));
        presenter.$wordTokens = presenter.$view.find(StringUtils.format(".{0}", presenter.defaults.css.selectableWord));
        presenter.$colorButtons = presenter.$view.find(StringUtils.format(".{0}", presenter.defaults.css.colorButton));
        presenter.$eraserButton = presenter.$view.find(StringUtils.format(".{0}", presenter.defaults.css.eraserButton));

        if (!presenter.configuration.showSetEraserButtonMode) {
            presenter.hideEraserButtonMode();
        }

        if (presenter.configuration.hideColorsButtons) {
            presenter.hideColorsButtons();
        }
    };

    presenter.hideEraserButtonMode = function () {
        presenter.$eraserButton.parent().hide();
    };

    presenter.hideColorsButtons = function () {
        presenter.$colorButtons.parent().hide();
    };

    presenter.createView = function () {
        var buttonsContainerHTML;
        var textHTML;
        var css;

        var mainContainer = "&lt;div class='{0}'&gt;";

        switch (presenter.configuration.buttonsPosition) {
            case presenter.POSITIONS.LEFT:
                css = presenter.getContainerCssByPosition(presenter.configuration.buttonsPosition);
                buttonsContainerHTML = presenter.getButtonsContainerHTML(css);
                textHTML = presenter.getTextHTML(presenter.configuration.textTokens, css.tokensContainer, presenter.configuration.mode);
                mainContainer = StringUtils.format(mainContainer, css.mainContainer);
                mainContainer += buttonsContainerHTML + textHTML;
                break;
            case presenter.POSITIONS.RIGHT:
                css = presenter.getContainerCssByPosition(presenter.configuration.buttonsPosition);
                textHTML = presenter.getTextHTML(presenter.configuration.textTokens, css.tokensContainer, presenter.configuration.mode);
                buttonsContainerHTML = presenter.getButtonsContainerHTML(css);
                mainContainer = StringUtils.format(mainContainer, css.mainContainer);
                mainContainer += textHTML + buttonsContainerHTML;
                break;
            case presenter.POSITIONS.TOP:
                css = presenter.getContainerCssByPosition(presenter.configuration.buttonsPosition);
                textHTML = presenter.getTextHTML(presenter.configuration.textTokens, css.tokensContainer, presenter.configuration.mode);
                buttonsContainerHTML = presenter.getButtonsContainerHTML(css);
                mainContainer = StringUtils.format(mainContainer, css.mainContainer);
                mainContainer += buttonsContainerHTML + textHTML;
                break;
            case presenter.POSITIONS.BOTTOM:
                css = presenter.getContainerCssByPosition(presenter.configuration.buttonsPosition);
                textHTML = presenter.getTextHTML(presenter.configuration.textTokens, css.tokensContainer, presenter.configuration.mode);
                buttonsContainerHTML = presenter.getButtonsContainerHTML(css);
                mainContainer = StringUtils.format(mainContainer, css.mainContainer);
                mainContainer += textHTML + buttonsContainerHTML;
                break;
        }

        mainContainer += "&lt;/div&gt;";
        return mainContainer;
    };

    presenter.getContainerCssByPosition = function (position) {
        var result = {
            eraserContainer: "",
            colorsContainer: "",
            mainContainer: "",
            buttonContainer: "",
            tokensContainer: ""
        };

        switch (position) {
            case presenter.POSITIONS.LEFT:
                result.eraserContainer = presenter.defaults.css.eraserButtonContainer.left;
                result.colorsContainer = presenter.defaults.css.colorButtonsContainer.left;
                result.mainContainer = presenter.defaults.css.mainContainer.left;
                result.buttonContainer = presenter.defaults.css.buttonsContainer.left;
                result.tokensContainer = presenter.defaults.css.tokensContainer.left;
                break;
            case presenter.POSITIONS.RIGHT:
                result.eraserContainer = presenter.defaults.css.eraserButtonContainer.right;
                result.colorsContainer = presenter.defaults.css.colorButtonsContainer.right;
                result.mainContainer = presenter.defaults.css.mainContainer.right;
                result.buttonContainer = presenter.defaults.css.buttonsContainer.right;
                result.tokensContainer = presenter.defaults.css.tokensContainer.right;
                break;
            case presenter.POSITIONS.TOP:
                result.eraserContainer = presenter.defaults.css.eraserButtonContainer.top;
                result.colorsContainer = presenter.defaults.css.colorButtonsContainer.top;
                result.mainContainer = presenter.defaults.css.mainContainer.top;
                result.buttonContainer = presenter.defaults.css.buttonsContainer.top;
                result.tokensContainer = presenter.defaults.css.tokensContainer.top;
                break;
            case presenter.POSITIONS.BOTTOM:
                result.eraserContainer = presenter.defaults.css.eraserButtonContainer.bottom;
                result.colorsContainer = presenter.defaults.css.colorButtonsContainer.bottom;
                result.mainContainer = presenter.defaults.css.mainContainer.bottom;
                result.buttonContainer = presenter.defaults.css.buttonsContainer.bottom;
                result.tokensContainer = presenter.defaults.css.tokensContainer.bottom;
                break
        }

        return result;
    };

    presenter.getButtonsContainerHTML = function (css) {
        var colorsButtons = presenter.getColorsButtonsHTML(presenter.configuration.colors, css.colorsContainer);
        var eraserButtons = presenter.getEraserModeButtonHTML(css.eraserContainer);
        return StringUtils.format("&lt;div class='{0}'&gt;{1} {2}&lt;/div&gt;", css.buttonContainer, colorsButtons, eraserButtons);
    };

    presenter.getColorsButtonsHTML = function (colorsDefinitions, containerCssClass) {
        var result = StringUtils.format("&lt;div class='{0}'&gt;", containerCssClass);

        var colorsLen = colorsDefinitions.length;
        for (var i = 0; i &lt; colorsLen; i++) {
            result = StringUtils.format("{0}{1}", result, presenter.getColorHTMLText(colorsDefinitions[i]));
        }

        result += "&lt;/div&gt;";

        return result;
    };

    presenter.getEraserModeButtonHTML = function (containerCssClass) {
        var result = StringUtils.format("&lt;div class='{0}'&gt;", containerCssClass);
        result += StringUtils.format("&lt;div class='{0}'&gt;{1}&lt;/div&gt;", presenter.defaults.css.eraserButton, presenter.configuration.eraserButtonText);
        result += "&lt;/div&gt;";

        return result;
    };

    presenter.getColorHTMLText = function (colorDefinition) {
        return StringUtils.format("&lt;div class='{0}' data-{1}='{2}'&gt;{3}&lt;/div&gt;", presenter.defaults.css.colorButton, presenter.defaults.dataHolders.colorID, colorDefinition.id, colorDefinition.description);
    };

    presenter.getTextHTML = function (tokens, containerCssClass, mode) {
        var tokensLen = tokens.length;
        var result = StringUtils.format("&lt;div class='{0}'&gt;", containerCssClass);

        for (var i = 0, wordIndex = 0; i &lt; tokensLen; i++) {
            if (tokens[i].type == presenter.TOKENS_TYPES.NEW_LINE) {
                result = StringUtils.format("{0}{1}", result, presenter.getNewLineHTML());
            } else if (tokens[i].type == presenter.TOKENS_TYPES.SPACE) {
                result = StringUtils.format("{0}{1}", result, presenter.getSpaceHTML());
            } else if ([presenter.TOKENS_TYPES.SELECTABLE, presenter.TOKENS_TYPES.INTRUDER].includes(tokens[i].type)) {
                result = StringUtils.format("{0}{1}", result, presenter.getWordHTML(tokens[i], wordIndex));
                wordIndex++;
            } else {
                if (mode == "ALL_SELECTABLE") {
                    result = StringUtils.format("{0}{1}", result, presenter.getWordHTML(tokens[i], wordIndex));
                } else {
                    result = StringUtils.format("{0}{1}", result, tokens[i].value);
                }
                wordIndex++;
            }
        }

        result += "&lt;/div&gt;";
        return result;
    };

    presenter.getNewLineHTML = function () {
        return "&lt;br&gt;";
    };

    presenter.getSpaceHTML = function () {
        return "&lt;span&gt; &lt;/span&gt;";
    };

    presenter.getWordHTML = function (token, index) {
        return StringUtils.format("&lt;span class='{0}' data-word-index='{1}' &gt;{2}&lt;/span&gt;",
            presenter.defaults.css.selectableWord, index, token.value);
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = upgradeModelAddProperties(model);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        upgradedModel = presenter.upgradeLangTag(upgradedModel);
        upgradedModel = presenter.upgradeIsNotActivity(upgradedModel);
        return upgradedModel;
    };

    function upgradeModelAddProperties(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['Mode']){
            upgradedModel['Mode'] = presenter.MODE.DEFAULT;
        }
        if(!upgradedModel['countErrors']) {
            upgradedModel['countErrors'] = false;
        }
        if(!upgradedModel['showAllAnswersInGradualShowAnswersMode']) {
            upgradedModel['showAllAnswersInGradualShowAnswersMode'] = false;
        }
        if(!upgradedModel['Legend title']) {
            upgradedModel['Legend title'] = 'Legend';
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function AddonText_Coloring_upgradeSpeechTexts (model) {
         let defaultValue = {
             Selected: {Selected: ""},
             Deselected: {Deselected: ""},
             TextContent: {TextContent: ""},
             Correct: {Correct: ""},
             Incorrect: {Incorrect: ""}
        };

        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object
        if (model["speechTexts"] == undefined) {
            upgradedModel["speechTexts"] = defaultValue;
        }

        return upgradedModel;
    };

    presenter.upgradeLangTag = function AddonText_Coloring_upgradeLangTag (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);
        if (model["langAttribute"] == undefined) {
            upgradedModel["langAttribute"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeIsNotActivity = function AddonText_Coloring_upgradeIsNotActivity (model) {
        const upgradedModel = {};
        jQuery.extend(true, upgradedModel, model);

        if (!model.hasOwnProperty('isNotActivity')) {
            upgradedModel['isNotActivity'] = 'False';
        }

        return upgradedModel;
    };

    presenter.setSpeechTexts = function AddonText_Coloring_setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            selected: presenter.DEFAULT_TTS_PHRASES.selected,
            deselected: presenter.DEFAULT_TTS_PHRASES.deselected,
            textContent: presenter.DEFAULT_TTS_PHRASES.textContent,
            correct: presenter.DEFAULT_TTS_PHRASES.correct,
            incorrect: presenter.DEFAULT_TTS_PHRASES.incorrect
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            selected: TTSUtils.getSpeechTextProperty(
                speechTexts.Selected.Selected,
                presenter.speechTexts.selected),
            deselected: TTSUtils.getSpeechTextProperty(
                speechTexts.Deselected.Deselected,
                presenter.speechTexts.deselected),
            textContent: TTSUtils.getSpeechTextProperty(
                speechTexts.TextContent.TextContent,
                presenter.speechTexts.textContent),
            correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.correct),
            incorrect: TTSUtils.getSpeechTextProperty(
                speechTexts.Incorrect.Incorrect,
                presenter.speechTexts.incorrect)
        };
    };

    presenter.validateModel = function (model) {
        var validatedIsNotActivity = ModelValidationUtils.validateBoolean(model['isNotActivity']);
        var validatedColors = presenter.validateColors(model.colors);
        if (validatedColors.isError) {
            return validatedColors;
        }

        var mode = ModelValidationUtils.validateOption(presenter.MODE, model.Mode);

        var parsedText = presenter.parseText(model.text, mode);
        var validatedText = presenter.validateUsingOnlyDefinedColors(parsedText, validatedColors.value);
        var modelText = model.text ? model.text.split(' ') : '';
        var height = model.Height ? model.Height / 2 : 0;
        var legendTitle = model['Legend title'] ?  model['Legend title'] : 'Legend';

        if (validatedText.isError) {
            return validatedText;
        }

        return {
            ID: model["ID"],
            isValid: true,
            isError: false,
            textTokens: parsedText,
            colors: validatedColors.value,
            buttonsPosition: presenter.parseButtonsPosition(model.buttonsPosition),
            showAllAnswersInGradualShowAnswersMode: ModelValidationUtils.validateBoolean(model.showAllAnswersInGradualShowAnswersMode),
            showSetEraserButtonMode: ModelValidationUtils.validateBoolean(model.showSetEraserModeButton),
            hideColorsButtons: ModelValidationUtils.validateBoolean(model.hideColorsButtons),
            eraserButtonText: presenter.parseEraserButtonText(model.eraserButtonText),
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            mode: mode,
            countErrors: ModelValidationUtils.validateBoolean(model.countErrors),
            modelText: modelText,
            height: height,
            legendTitle: legendTitle,
            langTag: model.langAttribute,
            isNotActivity: validatedIsNotActivity
        };
    };

    presenter.validateUsingOnlyDefinedColors = function (text, colors) {
        var tokenHaveInvalidColor = text.filter(filterSelectableTokens).some(function (token) {
            var foundColorDefinition = colors.some(function (colorDefinition) {
                return token.color == colorDefinition.id;
            });

            return !foundColorDefinition;
        });

        if (tokenHaveInvalidColor) {
            return ModelErrorUtils.getErrorObject(presenter.ERROR_CODES_KEYS.TC_TEXT_COLOR_DEFINITION_WRONG_ID);
        }

        return {
            isValid: true,
            isError: false
        };
    };

    presenter.parseEraserButtonText = function (eraserButtonText) {
        if (ModelValidationUtils.isStringEmpty(eraserButtonText)) {
            return presenter.defaults.eraserButtonDefaultText;
        } else {
            return eraserButtonText.trim();
        }
    };

    presenter.parseButtonsPosition = function (buttonsPosition) {
        if (buttonsPosition === undefined) {
            return "top";
        }

        if (ModelValidationUtils.isStringEmpty(buttonsPosition.trim())) {
            return "top";
        }

        return buttonsPosition.trim();
    };

    presenter.validateColors = function (colorsDefinitions) {
        var parsedValues = colorsDefinitions.map(markAsValidValues).map(parseIDs).map(parseRGBHex).map(parseDescriptions);

        var errorDefinitions = parsedValues.filter(filterInvalid);
        if (errorDefinitions.length &gt; 0) {
            return ModelErrorUtils.getErrorObject(errorDefinitions[0].errorCode);
        }

        return {
            isValid: true,
            value: parsedValues.map(removeIsValidFlag)
        };
    };

    presenter.parseText = function (text, mode) {
        if (ModelValidationUtils.isStringEmpty(text.trim())) {
            return [];
        }

        var groups = text.trim().split("\n").map(function (element) {
            return element.trim();
        });
        var groupsLen = groups.length;
        var result = [];
        var lastGroup = groups.length - 1;
        for (var groupNumber = 0; groupNumber &lt; groupsLen; groupNumber++) {
            var wordGroup = groups[groupNumber];
            if (wordGroup.length == 0) {
                result.push(getNewLineToken());
                continue;
            }

            var parsedWords = presenter.splitGroupToWords(groups[groupNumber], mode)
                .map(function (text) {return presenter.parseWords(text, mode);})
                .reduce(flattenArrays, []);

            if (parsedWords[parsedWords.length - 1].type == presenter.TOKENS_TYPES.SPACE) {
                parsedWords = parsedWords.slice(0, -1);
            }

            if (groupNumber != lastGroup) {
                result = result.concat(parsedWords, getNewLineToken());
            } else {
                result = result.concat(parsedWords);
            }
        }

        return result;
    };

    presenter.splitGroupToWords = function (group, mode) {
        if (mode == "ALL_SELECTABLE") {
            return group.split(" ").map(function (element) {
                element.trim();
                return element;
            }).filter(function (element) {
                return element != "";
            });
        } else {
            const splitGroup = [];

            const spaceRegExpSource = / /.source;
            const selectableRegExpSource = /\\color{[^}]+}{[^}]+}[^\s]*/.source;
            const intruderRegExpSource = /\\intruder{[^}]+}[^\s]*/.source;
            const mainRegExp = new RegExp([selectableRegExpSource, spaceRegExpSource, intruderRegExpSource].join('|'));

            let match = mainRegExp.exec(group);
            while (match !== null) {
                const before = group.substring(0, match.index);
                group = group.substring(match.index + match[0].length);

                if (before.trim().length &gt; 0) {
                    splitGroup.push(before.trim());
                }
                if (match[0].trim().length &gt; 0) {
                    splitGroup.push(match[0].trim());
                }

                match = mainRegExp.exec(group);
            }
            if (group.trim().length &gt; 0) {
                splitGroup.push(group.trim());
            }
            return splitGroup;
        }
    };

    presenter.parseWords = function (text, mode) {
        const result = [];
        const selectableRegExpSource = /\\color{(?&lt;color&gt;[^}]+)}{(?&lt;colorValue&gt;[^}]+)}/.source;
        const intruderRegExpSource = /\\intruder{(?&lt;intruderValue&gt;[^}]+)}/.source;
        const regExps = [selectableRegExpSource];
        if (mode === "MARK_PHRASES") {
            regExps.push(intruderRegExpSource);
        }

        const selectablePart = {
            setUpWord: function (word) {
                this.word = word;
                this.mixRegExp = new RegExp(regExps.join('|'), "g");
                this.mixRegExpMatch = this.mixRegExp.exec(word);
            },

            isSelectableMatch: function () {
                return !!this.mixRegExpMatch.groups.colorValue;
            },

            getSelectableTokenColor: function () {
                return this.mixRegExpMatch.groups.color;
            },

            getSelectableTokenValue: function () {
                return this.mixRegExpMatch.groups.colorValue;
            },

            isIntruderMatch: function () {
                return !!this.mixRegExpMatch.groups.intruderValue;
            },

            getIntruderTokenValue: function () {
                return this.mixRegExpMatch.groups.intruderValue;
            },

            getStartIndex: function () {
                return this.mixRegExpMatch.index;
            },

            getLastIndex: function () {
                return this.mixRegExp.lastIndex;
            },

            getPhrase: function () {
                return this.word.substring(0, this.getLastIndex());
            },

            isExists: function () {
                return this.mixRegExpMatch !== null;
            },

            hasPrecedingWord: function () {
                return this.getStartIndex() &gt; 0;
            },

            separatePrecedingWord: function () {
                return this.word.substring(0, this.getStartIndex());
            }
        };

        selectablePart.setUpWord(text);
        while (selectablePart.isExists()) {
            if (selectablePart.hasPrecedingWord()) {
                const precedingWord = selectablePart.separatePrecedingWord();
                result.push(getWordToken(precedingWord));

                text = text.substring(selectablePart.getStartIndex(), text.length);
                selectablePart.setUpWord(text);
            }

            const selectablePhrase = selectablePart.getPhrase();
            if (selectablePhrase.length &gt; 0) {
                if (selectablePart.isSelectableMatch()) {
                    const tokenWord = selectablePart.getSelectableTokenValue();
                    const tokenColor = selectablePart.getSelectableTokenColor();
                    result.push(getSelectableToken(tokenWord, tokenColor));
                } else if (selectablePart.isIntruderMatch()) {
                    const tokenWord = selectablePart.getIntruderTokenValue();
                    result.push(getIntruderToken(tokenWord));
                }
            }

            text = text.substring(selectablePart.getLastIndex(), text.length);
            selectablePart.setUpWord(text);
        }

        if (text.length &gt; 0) {
            result.push(getWordToken(text));
        }
        result.push(getSpaceToken());

        return result;
    };

    presenter.connectHandlers = function () {
        presenter.$colorButtons.hover(presenter.hoverInAddPointerHandler, presenter.hoverOutRemovePointerHandler);
        presenter.$colorButtons.click(presenter.selectColorButtonHandler);

        presenter.$eraserButton.hover(presenter.hoverInAddPointerHandler, presenter.hoverOutRemovePointerHandler);
        presenter.$eraserButton.click(presenter.activateEraserMode);
    };

    presenter.activateEraserMode = function (event) {
        deleteActiveClass();
        presenter.setEraserMode(event);
    };

    presenter.disconnectHandlers = function () {
        presenter.$colorButtons.off();
        presenter.$eraserButton.off();

        if (presenter.configuration.eraserMode) {
            presenter.disconnectWordTokensHandlers();
            presenter.toggleEraserMode();
        } else if (presenter.configuration.activeColorID !== null) {
            presenter.disconnectWordTokensHandlers();
            presenter.configuration.activeColor = null;
            presenter.configuration.activeColorID = null;
        }
    };

    presenter.connectWordTokensHandlers = function () {
        presenter.$wordTokens.hover(presenter.hoverInAddPointerHandler, presenter.hoverOutRemovePointerHandler);
        presenter.$wordTokens.hover(presenter.hoverInAddBackgroundHandler, presenter.hoverOutRemoveBackgroundHandler);
        presenter.$wordTokens.click(presenter.underlineWordHandler);
    };

    presenter.disconnectWordTokensHandlers = function () {
        presenter.$wordTokens.off();
    };

    presenter.eraserButtonClickHandler = function (event) {
        presenter.setEraserMode();
    };

    presenter.toggleEraserMode = function () {
        presenter.configuration.eraserMode = !presenter.configuration.eraserMode;
    };

    presenter.hoverInAddPointerHandler = function (event) {
        $(this).addClass(presenter.defaults.css.pointer);
    };

    presenter.hoverOutRemovePointerHandler = function (event) {
        $(this).removeClass(presenter.defaults.css.pointer);
    };

    presenter.hoverInAddBackgroundHandler = function (event) {
        $(this).addClass(presenter.defaults.css.hover);
    };

    presenter.hoverOutRemoveBackgroundHandler = function (event) {
        $(this).removeClass(presenter.defaults.css.hover);
    };

    presenter.resetColoredWords = function () {
        presenter.$view.find("[class*='text-coloring-colored-with']").each(function () {
            $(this).removeClass(function (index, css) {
                return (css.match(/\btext-coloring-colored-with\S+/g) || []).join(' ');
            });
        })
    };

    presenter.resetColoredWord = function (element) {
        $(element).removeClass(function (index, css) {
            return (css.match(/\btext-coloring-colored-with\S+/g) || []).join(' ');
        });
    };

    presenter.underlineWordHandler = function (event) {
        var $element = $(this);
        var wordIndex = $element.data("word-index");
        var tokenData = presenter.configuration.filteredTokens[wordIndex];
        if (presenter.configuration.eraserMode || (tokenData.selectionColorID == presenter.configuration.activeColorID)) {
            presenter.unmarkToken($element);
            presenter.removeColoredWordCss($element, presenter.configuration.filteredTokens[wordIndex]);
            presenter.removeColorData($element);
            presenter.sendErasingEvent(wordIndex);
        } else if (presenter.configuration.activeColor !== null &amp;&amp; tokenData.selectionColorID !== presenter.configuration.activeColorID) {
            presenter.markToken($element, presenter.configuration.activeColor);
            presenter.addColorData($element);
            presenter.sendMarkingEvent(wordIndex);
            presenter.resetColoredWord($element);
            presenter.addColoredWordCss($element, presenter.configuration.filteredTokens[wordIndex]);
        }


        if (presenter.isAllOK()) {
            presenter.sendAllOKEvent();
        }

        presenter.stateMachine.notifyEdit();
    };

    presenter.addColoredWordCss = function ($element, colorData) {
        var colorID = colorData.selectionColorID;
        $element.addClass(StringUtils.format(presenter.defaults.css.coloredWord, colorID))
    };

    presenter.removeColoredWordCss = function ($element, colorData) {
        var colorID = colorData.selectionColorID;
        $element.removeClass(StringUtils.format(presenter.defaults.css.coloredWord, colorID))
    };

    presenter.unmarkToken = function ($element) {
        $element.css({
            "text-decoration": "none",
            "padding-bottom": "none",
            "border-bottom": "none"
        });
    };

    presenter.markToken = function ($element, color) {
        $element.css({
            "text-decoration": "none",
            "padding-bottom": "0.1em",
            "border-bottom": StringUtils.format("0.1em solid {0}", color)
        });
    };


    presenter.hideTokenClasses = function ($element) {
        if (!$element) return;
        $element.each(function(){
            var $this = $(this);
            var classNames = $this.attr('class').split(/\s+/);
            var disabledClasses = [];
            for (var i = 0; i &lt; classNames.length; i++) {
                if (classNames[i] != presenter.defaults.css.selectableWord){
                    $this.removeClass(classNames[i]);
                    disabledClasses.push(classNames[i]);
                }
            }
            this.dataset.disabledClasses = disabledClasses.join(' ');
        });
    };

    presenter.showTokenClasses = function ($element) {
        if (!$element) return;
        $element.each(function(){
            var $this = $(this);
            var disabledClasses = this.dataset.disabledClasses;
            if (disabledClasses) {
                var classNames = disabledClasses.split(/\s+/);
                for (var i = 0; i &lt; classNames.length; i++) {
                    $this.addClass(classNames[i]);
                }
                this.dataset.disabledClasses = '';
            }
        });
    };

    presenter.addColorData = function ($element) {
        presenter.setTokenData($element, true, presenter.configuration.activeColorID);
    };

    presenter.removeColorData = function ($element) {
        presenter.setTokenData($element, false, null);
    };

    presenter.setTokenData = function ($element, isSelected, colorID) {
        var tokenIndex = $element.data("word-index");
        presenter.configuration.filteredTokens[tokenIndex].isSelected = isSelected;
        presenter.configuration.filteredTokens[tokenIndex].selectionColorID = colorID;
    };

    presenter.selectColorButtonHandler = function (event) {
        var $button = $(this);
        presenter.setColor($button.data(presenter.defaults.dataHolders.colorID));
    };

    presenter.shouldActivateColoringMode = function () {
        return presenter.configuration.activeColorID == null;
    };

    presenter.shouldDisableColoringMode = function (colorID) {
        return colorID == presenter.configuration.activeColorID;
    };

    presenter.activateColoringMode = function (colorID) {
        presenter.setActiveColor(colorID);
        presenter.connectWordTokensHandlers();
    };

    presenter.disableColoringMode = function () {
        presenter.configuration.activeColor = null;
        presenter.configuration.activeColorID = null;
        presenter.disconnectWordTokensHandlers();
    };

    presenter.setActiveColor = function (colorID) {
        var activeColorDefinition = presenter.getColorDefinitionById(colorID);

        presenter.configuration.activeColor = activeColorDefinition.color;
        presenter.configuration.activeColorID = colorID;
    };

    presenter.getColorDefinitionById = function (colorID) {
        return presenter.configuration.colors.filter(function (colorDefinition) {
            return colorDefinition.id == colorID;
        })[0];
    };

    presenter.onEventReceived = function (eventName, data) {
        if (presenter.configuration.isNotActivity) {
            return;
        }

        if (eventName == "ShowAnswers") {
            presenter.stateMachine.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.stateMachine.hideAnswers();
        }

        if (eventName == "GradualShowAnswers") {
            if (presenter.configuration.ID == data.moduleID) {
                presenter.stateMachine.gradualBlockAnswers();
                presenter.stateMachine.gradualShowAnswers();
            } else {
                presenter.stateMachine.gradualBlockAnswers();
            }
        }

        if (eventName == "GradualHideAnswers") {
            presenter.stateMachine.gradualHideAnswers();
        }
    };

    presenter.getActivitiesCount = function () {
        if (!presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            var answersNumber = 0;
            presenter.configuration.filteredTokens.filter(filterSelectableTokens).forEach(function (token) {
                var colorDefinition = presenter.getColorDefinitionById(token.color);
                if (colorDefinition !== undefined) {
                    answersNumber++;
                }
            });
            return answersNumber;
        }
        return 1;
    };

    presenter.getWordTokenByIndex = function (tokenIndex) {
        if (presenter.configuration.mode == "ALL_SELECTABLE") {
            return $(presenter.$wordTokens[tokenIndex]);
        } else {
            return presenter.$view.find('[data-word-index='+tokenIndex+']');
        }
    };

    presenter.reset = function () {
        presenter.stateMachine.reset();
        presenter.stateMachine.notifyEdit();
        presenter.setVisibility(presenter.initialVisibility);
    };

    presenter.setWorkMode = function () {
        if (presenter.configuration.isNotActivity) {
            return;
        }

        presenter.stateMachine.hideAnswers();
        presenter.stateMachine.onUnCorrect();
        presenter.stateMachine.notifyEdit();

        if (presenter.configuration.activeColorID !== null) {
            presenter.configuration.activeColorID = null;
            presenter.configuration.activeColor = null;
        }
        presenter.disconnectWordTokensHandlers();
        if  (presenter.stateMachine.previousActiveColorID !== null) {
            presenter.setColor(presenter.stateMachine.previousActiveColorID);
        } else {
            presenter.setColor(presenter.configuration.colors[0].id);
        }
    };

    presenter.setShowErrorsMode = function () {
        if(presenter.configuration.isNotActivity) {
            return;
        }

        if (isCheckAnswersActive()) {
            return;
        }

        presenter.stateMachine.check();
    };

    function isCheckAnswersActive() {
        return (presenter.stateMachine._actualState === StatefullAddonObject._internal.STATE.CORRECT
            || presenter.stateMachine._actualState === StatefullAddonObject._internal.STATE.WRONG);
    }

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'setEraserMode': presenter.setEraserMode,
            'setColor': presenter.setColorCommand,
            'isAttempted': presenter.isAttempted
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setColorCommand = function (args) {
        var validatedArgs = presenter.validateColorCommandArgs(args);

        if (validatedArgs.isError) {
            return;
        }

        presenter.setColor(validatedArgs.value);
    };

    presenter.validateColorCommandArgs = function (args) {
        if (args.length != 1) {
            return ModelErrorUtils.getErrorObject("");
        }

        var colorDefinition = presenter.getColorDefinitionById(args[0]);

        if (colorDefinition == undefined) {
            return ModelErrorUtils.getErrorObject("");
        }

        return {
            isError: false,
            value: args[0]
        };
    };

    presenter.setEraserMode = function () {
        if (!presenter.configuration.eraserMode) {
            presenter.disableColoringMode();
            presenter.connectWordTokensHandlers();
            presenter.toggleEraserMode();
        }
        presenter.setEraserButtonAsActive();
    };

    presenter.setColor = function (colorID) {
        if (presenter.configuration.eraserMode) {
            presenter.toggleEraserMode();
            presenter.setActiveColor(colorID);
            presenter.unsetEraserButtonAsActive();
            presenter.setColorButtonAsActive(colorID);
        } else if (presenter.shouldActivateColoringMode()) {
            presenter.activateColoringMode(colorID);
            presenter.setColorButtonAsActive(colorID);
        } else {
            presenter.setActiveColor(colorID);
            presenter.setColorButtonAsActive(colorID);
        }
    };
 
     presenter.isAttempted = function () {
        var words = presenter.configuration.filteredTokens;
        var attempted = false;
        for (var i = 0; i &lt; words.length; i++) {
            if (words[i].isSelected) {
                attempted = true;
                break;
            }
        };
        return attempted;
    };

    presenter.setEraserButtonAsActive = function () {
        if (presenter.$colorButtons !== null) {
            presenter.$colorButtons.removeClass(presenter.defaults.css.activeButton);
        }

        if (presenter.$eraserButton !== null) {
            presenter.$eraserButton.addClass(presenter.defaults.css.activeButton);
        }
    };

    presenter.unsetEraserButtonAsActive = function () {
        if (presenter.$eraserButton !== null) {
            presenter.$eraserButton.removeClass(presenter.defaults.css.activeButton);
        }
    };

    presenter.unsetColorButtonsAsActive = function () {
        presenter.$colorButtons.removeClass(presenter.defaults.css.activeButton);
    };

    presenter.setColorButtonAsActive = function (colorID) {
        var button = presenter.getColorButtonByID(colorID);
        if (button !== null) {
            presenter.unsetColorButtonsAsActive();
            $(button).addClass(presenter.defaults.css.activeButton);
        }
    };

    presenter.getColorButtonByID = function (colorID) {
        var colorButton = $.makeArray(presenter.$colorButtons).filter(function (button) {
            return $(button).data(presenter.defaults.dataHolders.colorID) == colorID;
        });

        if (colorButton.length &gt; 0) {
            return colorButton;
        } else {
            return null;
        }
    };

    presenter.show = function () {
        presenter.configuration.isVisible = true;
        presenter.$view.css("visibility", "visible");
    };

    presenter.hide = function () {
        presenter.configuration.isVisible = false;
        presenter.$view.css("visibility", "hidden");
    };

    presenter.sendErasingEvent = function (wordIndex) {
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': (wordIndex + 1),
            'value': presenter.EVENT_TYPES.DESELECTING,
            'score': 0
        });
    };

    presenter.sendMarkingEvent = function (wordIndex) {
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': (wordIndex + 1),
            'value': presenter.EVENT_TYPES.SELECTING,
            'score': presenter.getScoreForWordMarking(wordIndex, presenter.configuration.activeColorID)
        });
    };

    presenter.getScoreForWordMarking = function (wordIndex, colorID) {
        if (presenter.configuration.filteredTokens[wordIndex].color == colorID) {
            return 1;
        }

        return 0;
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        return presenter.configuration.filteredTokens.filter(filterSelectableTokens).length;
    };

    presenter.getErrorCount = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        return presenter.configuration.filteredTokens.filter(filterSelectedTokens).filter(filterWrongTokens).length;
    };

    presenter.getScore = function () {
        if (presenter.configuration.isNotActivity) {
            return 0;
        }

        return presenter.getMaxScore() - presenter.configuration.filteredTokens.filter(filterSelectableTokens).filter(filterWrongTokens).length;
    };

    presenter.getState = function () {
        var activeColorID = presenter.configuration.activeColorID;
        if (!presenter.configuration.eraserMode &amp;&amp; activeColorID === null) {
            activeColorID = presenter.stateMachine.previousActiveColorID;
        }
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible,
            tokens: presenter.configuration.filteredTokens,
            activeColorID: activeColorID
        });
    };

    presenter.setState = function (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) {
            return;
        }

        var parsedState = JSON.parse(stateString);

        presenter.configuration.filteredTokens = parsedState.tokens;
        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.colorAllMarkedTokens();

        if (parsedState.activeColorID !== undefined) {
            if (parsedState.activeColorID == null) {
                presenter.setEraserMode();
            } else {
                presenter.setColor(parsedState.activeColorID);
            }
        }
    };

    presenter.setVisibility = function (isVisible) {
        if (isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    };

    presenter.colorAllMarkedTokens = function () {
        presenter.configuration.filteredTokens.filter(function (token) {
            return token.selectionColorID !== null;
        }).forEach(function (token) {
            var $token = presenter.getWordTokenByIndex(token.index);
            var colorDefinition = presenter.getColorDefinitionById(token.selectionColorID);
            presenter.markToken($token, colorDefinition.color);
            presenter.addColoredWordCss($token, token);
        });
    };

    presenter.sendAllOKEvent = function () {
        var eventData = {
            'source': presenter.configuration.ID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.isAllOK = function () {
        if (presenter.configuration.countErrors) {
            return presenter.getMaxScore() === presenter.getScore() - presenter.getErrorCount();
        } else {
            return presenter.configuration.filteredTokens.filter(filterSelectableTokens).every(function (token) {
                return presenter.getScoreForWordMarking(token.index, token.selectionColorID);
            });
        }

    };

    presenter.addCorrectClass = function (tokenIndex) {
        presenter.getWordTokenByIndex(tokenIndex).addClass(presenter.defaults.css.markings.correct);
    };

    presenter.addWrongClass = function (tokenIndex) {
        presenter.getWordTokenByIndex(tokenIndex).addClass(presenter.defaults.css.markings.wrong);
    };

    presenter.removeAllCheckAnswersClasses = function () {
        presenter.$wordTokens.removeClass(presenter.defaults.css.markings.correct);
        presenter.$wordTokens.removeClass(presenter.defaults.css.markings.wrong);
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var printableHTML = '';
        var userAnswer = presenter.getUserAnswer();
        var didUserAnswer = userAnswer ? userAnswer.some(answer =&gt; answer.isSelected) : false;
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        var modelText = presenter.configuration.textTokens.filter(token =&gt; ![presenter.TOKENS_TYPES.SPACE, presenter.TOKENS_TYPES.NEW_LINE].includes(token.type));
        var areAllSelectable = presenter.configuration.mode === 'ALL_SELECTABLE'

        modelText.forEach((token, index) =&gt; {
            var colorId;
            var color;
            var isSelectableToken = token.hasOwnProperty("color");
            var isIntruderToken = token.hasOwnProperty("type") &amp;&amp; token.type === presenter.TOKENS_TYPES.INTRUDER;
            switch (true) {
                case (userAnswer &amp;&amp; userAnswer[index].isSelected) &amp;&amp; !showAnswers:
                    colorId = userAnswer[index].selectionColorID;
                    color = presenter.getColor(model, colorId);

                    printableHTML += `&lt;span style="border: 2px solid ${color}"&gt;${userAnswer[index].value}&lt;/span&gt; `;
                    break;

                case (userAnswer &amp;&amp; userAnswer[index].isSelected) &amp;&amp; showAnswers:
                    colorId = userAnswer[index].selectionColorID;
                    color = presenter.getColor(model, colorId);

                    var answerMark = presenter.getPrintableMark(userAnswer[index].selectionColorID === userAnswer[index].color);
                    printableHTML += `&lt;span style="border: 2px solid ${color}"&gt;${userAnswer[index].value} ${answerMark}&lt;/span&gt; `;
                    break;

                case isSelectableToken &amp;&amp; showAnswers &amp;&amp; !didUserAnswer:
                    colorId = token.color;
                    color = presenter.getColor(model, colorId);

                    printableHTML += `&lt;span style="border: 2px dashed ${color}"&gt;${token.value}&lt;/span&gt; `;
                    break;

                case (isSelectableToken || isIntruderToken) &amp;&amp; (!areAllSelectable || showAnswers):
                    printableHTML += `&lt;span style="border-bottom: 1px solid"&gt;${token.value}&lt;/span&gt; `;
                    break;

                default:
                    printableHTML += `${token.value} `;
            }
        });

        return presenter.createHTML(showAnswers || userAnswer, printableHTML, presenter.configuration);
    }

    presenter.createHTML = function (shouldChangeLineHeight, htmlContent) {
        var $legend = $('&lt;div&gt;&lt;/div&gt;');
        var $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        var colors = presenter.configuration.colors;
        var height = presenter.configuration.height + (2 + colors.length) * 20;
        var legendTitle = presenter.configuration.legendTitle
        $wrapper.addClass('printable_addon_Paragraph');
        $wrapper.css("left", "0px");
        $wrapper.css("right", "0px");
        $wrapper.css("height", `${height}px`);
        $wrapper.css("padding", "10px 10px 10px 0px");
        var $paragraph = $('&lt;div&gt;&lt;/div&gt;');
        $paragraph.css("left", "0px");
        $paragraph.css("right", "0px");
        $paragraph.css("height", `${presenter.configuration.height}`);
        $paragraph.css("border", "1px solid");
        $paragraph.css("padding", "10px");
        if (shouldChangeLineHeight) {
            $paragraph.css("line-height", "1.5");
        }
        $paragraph.html(htmlContent);
        $legend.append(presenter.createLegend(colors,  legendTitle))
        $wrapper.append($paragraph);
        $wrapper.append($legend);
        return $wrapper[0].outerHTML;
    }

    /* Return colors in grayscale from the model */
    presenter.getColors = function (model) {
        var colors = [];
        model['colors'].forEach(colorObject =&gt; {
           colors.push({id: colorObject.id, value: parseToGrayscale(colorObject.color)});
        });

        return colors;
    }

    /* Returns color in hex code from color name */
    presenter.getColorInHex = function (model, colorId) {
        var colors =  presenter.getColors(model);

        return colors.find(color =&gt; color.id === colorId).value;
    }

    /* Extracts the word from phrase \\color{color}{word} */
    presenter.getWord = function (phrase) {
        var regExp = /(\\color{\w*}{)/;
        return phrase.replace(regExp, '').replace(/}/, '');
    }

    /* Extracts the color from phrase and convert into grayscale \\color{color}{word} */
    presenter.getColor = function (model, colorId) {
        return parseToGrayscale(presenter.getColorInHex(model, colorId));
    }

    /* Return user answers */
    presenter.getUserAnswer = function () {
        if (presenter.printableState &amp;&amp; presenter.printableState.hasOwnProperty('tokens')) {
            return presenter.printableState['tokens'];
        }

        return null;
    }

    presenter.getPrintableMark = function (isCorrectAnswer) {
        return isCorrectAnswer ? '&amp;#10004;' : '&amp;#10006;';
    }

    presenter.createLegend = function (colors, legendTitle) {
        var $table = $("&lt;table&gt;&lt;/table&gt;");
        var $tbody = $("&lt;tbody&gt;&lt;/tbody&gt;");

        $tbody.append($(`&lt;caption&gt;${legendTitle}&lt;/caption&gt;`));
        colors.forEach(colorObject =&gt; {
            var $tr = $("&lt;tr&gt;&lt;/tr&gt;");

            var $td = $("&lt;td&gt;&lt;div style='width: 20px; height: 10px'&gt;&lt;/div&gt;&lt;/td&gt;");
            $td.css('background', parseToGrayscale(colorObject.color));

            var $description = $(`&lt;td&gt;&lt;div style="padding-left: 10px"&gt;${colorObject.description}&lt;/div&gt;&lt;/td&gt;`);

            $tr.append($td);
            $tr.append($description);
            $tbody.append($tr);
        });
        $table.append($tbody);

        return $table
    }

    function TextColoringKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    TextColoringKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    TextColoringKeyboardController.prototype.constructor = TextColoringKeyboardController;

    presenter.buildKeyboardController = function AddonText_Coloring_buildKeyboardController () {
        presenter.keyboardControllerObject = new TextColoringKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getDefaultElementsStringForKeyboardNavigation = function AddonText_Coloring_getDefaultElementsStringForKeyboardNavigation () {
        if (presenter.configuration.showSetEraserButtonMode) {
            return ".text-coloring-color-button, .text-coloring-eraser-button, .text-coloring-selectable-word";
        } else {
            return ".text-coloring-color-button, .text-coloring-selectable-word";
        }
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find(presenter.getDefaultElementsStringForKeyboardNavigation());
    };

    presenter.getElementsForTTS = function () {
        const container = presenter.defaults.css.tokensContainer;
        const allPossibleContainerClasses = `.${container.bottom}, .${container.left}, .${container.top}, .${container.right}`;
        return presenter.$view.find(`${presenter.getDefaultElementsStringForKeyboardNavigation()}, ${allPossibleContainerClasses}`);
    };

    presenter.keyboardController = function AddonText_Coloring_keyboardController (keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    TextColoringKeyboardController.prototype.getTarget = function (element) {
        return $(element);
    };

    TextColoringKeyboardController.prototype.switchElement = function (move) {
        var new_position_index = this.keyboardNavigationCurrentElementIndex + move;
        if (new_position_index &gt;= this.keyboardNavigationElementsLen || new_position_index &lt; 0) {
            new_position_index = this.keyboardNavigationCurrentElementIndex;
        }

        this.markCurrentElement(new_position_index);
        this.readCurrentElement();
    };

    TextColoringKeyboardController.prototype.enter = function (event) {
        if (presenter.isTTS() &amp;&amp; presenter.isFirstEnter) {
            KeyboardController.prototype.setElements.call(this, presenter.getElementsForTTS());
            presenter.isFirstEnter = false;
        }
        KeyboardController.prototype.enter.call(this, event);
        this.readCurrentElement();
    };

     TextColoringKeyboardController.prototype.exitWCAGMode = function () {
        presenter.isFirstEnter = true;
        KeyboardController.prototype.setElements.call(this, presenter.getElementsForKeyboardNavigation());
        KeyboardController.prototype.exitWCAGMode.call(this);
    };

    TextColoringKeyboardController.prototype.selectAction = function () {
        if (!presenter.isAddonInWorkMode()) {
            return;
        }
        KeyboardController.prototype.selectAction.call(this);
        presenter.speakOnElementSelection();
    };

    presenter.speakOnElementSelection = function AddonText_Coloring_speakOnElementSelection () {
        const element = presenter.keyboardControllerObject.getTarget(presenter.keyboardControllerObject.keyboardNavigationCurrentElement);
        let text = ""
        if (element.hasClass(presenter.defaults.css.colorButton) || element.hasClass(presenter.defaults.css.eraserButton)) {
            text = presenter.speechTexts.selected;
        } else if (element.hasClass(presenter.defaults.css.selectableWord)) {
            const color = presenter.getColorObjectOrNoneFromElementClass(element);
            text = color ? `${presenter.speechTexts.selected} ${color.description}` : presenter.speechTexts.deselected;
        }

        if (text) {
            presenter.speak(text);
        }
    };

    TextColoringKeyboardController.prototype.readCurrentElement = function () {
        const element = this.getTarget(this.keyboardNavigationCurrentElement);
        let text = "";
        const elementContent = element[0].textContent;

        if (element.hasClass(presenter.defaults.css.colorButton) || element.hasClass(presenter.defaults.css.eraserButton)) {
            text = presenter.getTTSForColorButton(element, elementContent);
        } else if (element[0].className.includes("text-coloring-tokens-container")) {
            text = presenter.getTTSForContent();
        } else if (element.hasClass(presenter.defaults.css.selectableWord)) {
            text = presenter.getTTSForWord(element, elementContent);
        }

        presenter.speak(text);
    };

    presenter.getTTSForColorButton = function AddonText_Coloring_getTTSForColorButton (element, text) {
        const selectedText = presenter.isColorButtonClicked(element) ? presenter.speechTexts.selected : "";
        return `${text} ${selectedText}`;
    };

    presenter.getTTSForContent = function AddonText_Coloring_getTTSForContent () {
        const tts = [TTSUtils.getTextVoiceObject(presenter.speechTexts.textContent)];
        let text = "";

        presenter.configuration.filteredTokens.forEach((token) =&gt; {
            let tokenContent = $("&lt;p&gt;" + token.value + "&lt;\p&gt;")[0].textContent;
            text += tokenContent + " ";
            const tokenElement = presenter.getWordTokenByIndex(token.index);
            if (!tokenElement.length) {
                return;
            }
            const ttsColoring = presenter.getWordTTSAttributes(tokenElement);

            if (ttsColoring) {
                tts.push(TTSUtils.getTextVoiceObject(text, presenter.configuration.langTag));
                text = " ";
                tts.push(TTSUtils.getTextVoiceObject(ttsColoring));
            }
        });

        tts.push(TTSUtils.getTextVoiceObject(text, presenter.configuration.langTag));

        return tts;
    };

    presenter.getTTSForWord = function AddonText_Coloring_getTTSForWord (element, word) {
        const tts = [TTSUtils.getTextVoiceObject(word, presenter.configuration.langTag)];

        let wordAttributes = presenter.getWordTTSAttributes(element);
        if (wordAttributes) {
            tts.push(TTSUtils.getTextVoiceObject(wordAttributes));
        }

        return tts;
    };

    presenter.getWordTTSAttributes = function AddonText_Coloring_getWordTTSAttributes (element) {
        let text = "";

        const color = presenter.getColorObjectOrNoneFromElementClass(element);
        if (color) {
           text += ` ${presenter.speechTexts.selected} ${color.description}`;
        }

        if (element.hasClass(presenter.defaults.css.markings.correct)) {
           text += ` ${presenter.speechTexts.correct}`;
        } else if (element.hasClass(presenter.defaults.css.markings.wrong)) {
           text += ` ${presenter.speechTexts.incorrect}`;
        }

        return text;
    };

    presenter.isColorButtonClicked = function AddonText_Coloring_isColorButtonClicked (element) {
        return element.hasClass(presenter.defaults.css.activeButton);
    };

    presenter.getColorObjectOrNoneFromElementClass = function AddonText_Coloring_getColorObjectOrNoneFromElementClass (element) {
        const classList = Array.from(element[0].classList);
        const underline = classList.find(c =&gt; c.includes("text-coloring-colored-with") || c.includes("text-coloring-show-answers"));
        if (!underline) {
            return;
        }

        const colorKey = underline.split("-").slice(-1)[0];
        return presenter.getColorDefinitionById(colorKey);
    };

    presenter.isAddonInWorkMode = function AddonText_Coloring_isAddonInWorkMode () {
        return presenter.stateMachine._actualState === StatefullAddonObject._internal.STATE.WORK;
    };

    presenter.speak = function AddonText_Coloring_speak(data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.setWCAGStatus = function AddonText_Coloring_setWCAGStatus(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    presenter.getTextToSpeechOrNull = function AddonText_Coloring_getTextToSpeechOrNull(playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.isTTS = function AddonText_Coloring_isTTS () {
        return presenter.getTextToSpeechOrNull(presenter.playerController) &amp;&amp; presenter.isWCAGOn;
    };

    return presenter;
}

function parseToGrayscale(hexColor) {
    var rgbColor = hexToRGB(hexColor);
    return rgbToGrayscaleHex(rgbColor);
}

function hexToRGB(hexColor) {
    var r = 0, g = 0, b = 0;

    if (hexColor.length === 4) {
        r = '0x' + hexColor[1] + hexColor[1];
        g = '0x' + hexColor[2] + hexColor[2];
        b = '0x' + hexColor[3] + hexColor[3];
    } else if (hexColor.length === 7) {
        r = '0x' + hexColor[1] + hexColor[2];
        g = '0x' + hexColor[3] + hexColor[4];
        b = '0x' + hexColor[5] + hexColor[6];
    }

    return [+r, +g, +b];
}

function rgbToGrayscaleHex(rgbColor) {
    var grayIntensity = Math.round(0.3 * rgbColor[0] + 0.59 * rgbColor[1] + 0.11 * rgbColor[2]);

    return '#' + decimalToHex(grayIntensity) + decimalToHex(grayIntensity) + decimalToHex(grayIntensity);
}

function decimalToHex(value) {
    var hex = value.toString(16);
    return hex.length === 1 ? `0${hex}` : hex;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Text_Selection" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Mode" nameLabel="Text_Selection_property_mode" type="{Mark phrases to select, All selectable}"/>
		<property name="Selection type" nameLabel="Text_Selection_property_selection_type" type="{Single select, Multiselect}"/>
		<property isDefault="true" isLocalized="true" name="Text" nameLabel="Text_Selection_property_text" type="html"/>
        <property name="Enable letters selections" nameLabel="Text_Selection_property_enable_letters_selections" type="boolean"/>
        <property displayName="Is Not Activity" name="isNotActivity" nameLabel="Text_Selection_property_is_not_activity" type="boolean"/>
		<property displayName="Enable scrolling on mobile" name="enableScroll" nameLabel="Text_Selection_property_enable_scroll" type="boolean"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Text_Selection_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Text_Selection_property_speech_texts" type="staticlist">

            <property name="selectedSectionStart" nameLabel="Text_Selection_selected_section_start" type="staticrow">
                <property name="selectedSectionStart" nameLabel="Text_Selection_selected_section_start" type="string"/>
            </property>

			<property name="selectedSectionEnd" nameLabel="Text_Selection_selected_section_end" type="staticrow">
                <property name="selectedSectionEnd" nameLabel="Text_Selection_selected_section_end" type="string"/>
            </property>

			<property name="deselected" nameLabel="Text_Selection_deselected" type="staticrow">
                <property name="deselected" nameLabel="Text_Selection_deselected" type="string"/>
            </property>

			<property name="selected" nameLabel="Text_Selection_selected" type="staticrow">
                <property name="selected" nameLabel="Text_Selection_selected" type="string"/>
            </property>

			<property name="wrong" nameLabel="Text_Selection_wrong" type="staticrow">
                <property name="wrong" nameLabel="Text_Selection_wrong" type="string"/>
            </property>

			<property name="correct" nameLabel="Text_Selection_correct" type="staticrow">
                <property name="correct" nameLabel="Text_Selection_correct" type="string"/>
            </property>

            <property name="phrase" nameLabel="Text_Selection_phrase" type="staticrow">
                <property name="phrase" nameLabel="Text_Selection_phrase" type="string"/>
            </property>

			<property name="phraseEnd" nameLabel="Text_Selection_phrase_end" type="staticrow">
                <property name="phraseEnd" nameLabel="Text_Selection_phrase_end" type="string"/>
            </property>
		</property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
	</model>
<css>.text_selection .selectable {
	border-radius: 5px;
}

.text_selection .selected {
	background-color: #FAFAD2;
}

.text_selection .wrong {
	background-color: #FF6347;
}

.text_selection .correct {
	background-color: #7CFC00;
}

.text_selection .hover {
	background-color: orange;
	cursor: pointer;
}

.text_selection div {
    min-height: 20px;
}

.printable_addon_Text_Selection_selected {
	text-decoration: underline;
	text-decoration-style: solid;
	text-decoration-color: black;
}

.printable_addon_Text_Selection_wrong::after {
	content: " \2716";
}

.printable_addon_Text_Selection_correct::after {
    content: " \2714";
}

.printable_addon_Text_Selection_correct_answer {
	text-decoration: underline;
	text-decoration-style: dotted;
	text-decoration-color: black;
}
</css><view/><preview/><presenter>function AddonText_Selection_create() {

    var presenter = function() {};

    presenter.eventBus = null;
    presenter.playerController = null;
    presenter.textParser = null;
    presenter.selected_elements = null;
    presenter.isWorkMode = true;
    presenter.markedMathJaxContent = [];
    presenter.areAllPhrasesSingleWord = true;
    presenter._keyboardController = null;
    presenter._firstElementSwitch = true;
    presenter.isGradualShowAnswersActive = false;
    presenter.printableState = null;
    presenter.printableStateMode = null;
    presenter.activeGradualShowAnswersItems = [];
    var isWCAGOn = false;

    var SELECTED_SECTION_START = "&amp;\n&amp;SELECTED_SECTION_START&amp;\n&amp;";
    var SELECTED_SECTION_END = "&amp;\n&amp;SELECTED_SECTION_END&amp;\n&amp;";
    var CORRECT_SECTION_START = "&amp;\n&amp;CORRECT_SECTION_START&amp;\n&amp;";
    var CORRECT_SECTION_END = "&amp;\n&amp;CORRECT_SECTION_END&amp;\n&amp;";
    var WRONG_SECTION_START = "&amp;\n&amp;WRONG_SECTION_START&amp;\n&amp;";
    var WRONG_SECTION_END = "&amp;\n&amp;WRONG_SECTION_END&amp;\n&amp;";
    var SELECTED ="&amp;\n&amp;SELECTED&amp;\n&amp;";
    var WRONG = "&amp;\n&amp;WRONG&amp;\n&amp;";
    var CORRECT = "&amp;\n&amp;CORRECT&amp;\n&amp;";
    var SPLIT = "&amp;\n&amp;SPLIT&amp;\n&amp;";
    var PHRASE = "&amp;\n&amp;PHRASE&amp;\n&amp;";
    var PHRASE_END = "&amp;\n&amp;PHRASE_END&amp;\n&amp;";

    var MATH_JAX_MARKER = 'MATHJAX';

    var DEFAULT_TTS_PHRASES = {
        selectedSectionStart: 'start of selected section',
        selectedSectionEnd: 'end of selected section',
        selected: 'selected',
        deselected: 'deselected',
        wrong: 'wrong',
        correct: 'correct',
        phrase: 'phrase',
        phraseEnd: 'end of phrase',
    };

    var CSS_CLASSES = {
        SELECTABLE: "selectable",
        SELECTED: "selected",
        DISABLED: "text_selection_disabled",
        PRINTABLE: "printable_addon_Text_Selection",
        PRINTABLE_SELECTED: "printable_addon_Text_Selection_selected",
        PRINTABLE_WRONG: "printable_addon_Text_Selection_wrong",
        PRINTABLE_CORRECT: "printable_addon_Text_Selection_correct",
        PRINTABLE_CORRECT_ANSWER: "printable_addon_Text_Selection_correct_answer",
    };

    presenter.setPlayerController = function (controller) {
        this.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        presenter.textParser = new TextParserProxy(getTextParser());
    };

    /**
     * @param controller (PrintableController)
     */
    presenter.setPrintableController = function (controller) {
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    }

    function getEventData(it, val, sc) {
        return {
            'source': presenter.configuration.addonID,
            'item': '' + it,
            'value': '' + val,
            'score': '' + sc
        }
    }

    presenter.sendEvent = function(item, value, score, checkIsAllOK) {
        presenter.eventBus.sendEvent('ValueChanged', getEventData(
            parseInt(item, 10) + 1,
            value ? 1 : 0,
            score ? 1 : 0
        ));

        if (presenter.isAllOK() &amp;&amp; checkIsAllOK &amp;&amp; presenter.configuration.isActivity) sendAllOKEvent();
    };

    function sendAllOKEvent() {
        presenter.eventBus.sendEvent('ValueChanged', getEventData('all', '', ''));
    }

    var first = 0,
        beforeActive = false,
        lastMoveEvent = null;

    function isLastSpecialSign(word) {
        return ['.', ',', '?', '!', ';', ':'].indexOf(word[word.length-1]) != -1;
    }

    presenter.isStartedCorrect = function(word) {
        return (/\\correct{/).test(word);
    };

    presenter.isStartedWrong = function(word) {
        return (/\\wrong{/).test(word);
    };

    presenter.hasOpeningBracket = function(word) {
        return (/{/).test(word);
    };

    presenter.hasClosingBracket = function(word) {
        return (/}/).test(word);
    };

    presenter.getWrongWords = function(word) {
        var pattern = (/(.*)\\wrong{(.*)}(.*)/);
        return pattern.exec(word).slice(1);
    };

    presenter.getCorrectWords = function(word) {
        var pattern = (/(.*)\\correct{(.*)}(.*)/);
        return pattern.exec(word).slice(1);
    };

    presenter.isMarkedCorrect = function(word) {
        var counted = this.countBrackets(word);
        return (/\\correct{.*}/).test(word) &amp;&amp; (counted.open === counted.close);
    };

    presenter.isMarkedWrong = function(word) {
        var counted = this.countBrackets(word);
        return (/\\wrong{.*}/).test(word) &amp;&amp; (counted.open === counted.close);
    };

    presenter.cutMarkedCorrect = function(word) {
        var countedBrackets = this.countBrackets(word);
        if (isLastSpecialSign(word)) {
            word = word.replace(/\\correct{/, '');
            if (countedBrackets.open === countedBrackets.close) {
                word = word.replace(/}([^}]*)$/,'$1');
            }
            return word.substring(0, word.length-1);
        } else {
            word = word.replace(/\\correct{/, '');
            if (countedBrackets.open === countedBrackets.close) {
                word = word.replace(/}([^}]*)$/,'$1');
            }
            return word;
        }
    };

    presenter.cutMarkedWrong = function(word) {
        var countedBrackets = this.countBrackets(word);
        if (isLastSpecialSign(word)) {
            word = word.replace(/\\wrong{/, '');
            if (countedBrackets.open === countedBrackets.close) {
                word = word.replace(/}([^}]*)$/,'$1');
            }
            return word.substring(0, word.length - 1);
        } else {
            word = word.replace(/\\wrong{/, '');
            if (countedBrackets.open === countedBrackets.close) {
                word = word.replace(/}([^}]*)$/,'$1');
            }
            return word;
        }
    };

    presenter.cutLastClosingBracket = function (word) {
        return word.replace(/}([^}]*)$/, '$1');
    };

    presenter.countBrackets = function (word) {
        return {
            open: word.split("{").length - 1,
            close: word.split("}").length - 1
        }
    };

    presenter.getSpanByNumber = function (number) {
        return presenter.$view.find('.text_selection').find(`span[number='${number}']`);
    };

    presenter.handleSingleSelection = function () {
        if (presenter.configuration.selection_type === 'SINGLESELECT') {
            presenter.handleSingleSelectionSingleSelect();
        } else if (presenter.configuration.selection_type === 'MULTISELECT') {
            presenter.handleSingleSelectionMultiSelect();
        }

        const $span = presenter.getSpanByNumber(first);
        if ($span.hasClass('selectable')) {
            presenter.sendEvent($span.attr('number'), $span.hasClass('selected'), isCorrect($span), true);
        }
    };

    presenter.handleSingleSelectionSingleSelect = function() {
        const selected = presenter.$view.find('.text_selection').find('.selected');
        const $span = presenter.getSpanByNumber(first);

        if (selected.length === 0) {
            if ($span.hasClass('selectable')) {
                $span.addClass('selected');
            }
        } else if (selected.length === 1) {
            if (parseInt(selected.attr('number'), 10) === parseInt(first, 10)) {
                selected.removeClass('selected');
            } else {
                if ($span.hasClass('selectable')) {
                    selected.removeClass('selected');
                    $span.toggleClass('selected');
                }
            }
        }
    };

    presenter.handleSingleSelectionMultiSelect = function() {
        const $span = presenter.getSpanByNumber(first);

        if ($span.hasClass('selectable')) {
            $span.toggleClass('selected');
        }
    };

    presenter.handleMultipleSelection = function (last) {
        const isSelectedRightToLeft = first &gt; last;
        if (isSelectedRightToLeft) [first, last] = [last, first];

        if (presenter.configuration.selection_type === 'SINGLESELECT') {
            presenter.handleMultipleSelectionSingleSelect(last, isSelectedRightToLeft);
        } else if (presenter.configuration.selection_type === 'MULTISELECT') {
            presenter.handleMultipleSelectionMultiSelect(last);
        }

        for (let i = first; i &lt; last + 1; i++) {
            const $span = presenter.getSpanByNumber(i);
            if ($span.hasClass('selectable')) {
                presenter.sendEvent($span.attr('number'), $span.hasClass('selected'), isCorrect($span), i === last);
            }
        }
    };

    presenter.handleMultipleSelectionSingleSelect = function (last, isSelectedRightToLeft) {
        const selected = presenter.$view.find('.text_selection').find('.selected');
        const $span = isSelectedRightToLeft ? this.getSpanByNumber(first) : this.getSpanByNumber(last);
        if ($span.hasClass('selectable')) {
            if (selected.length === 1) $(selected).removeClass('selected');
            $span.toggleClass('selected');
        }
    };

    presenter.handleMultipleSelectionMultiSelect = function (last) {
        for (let i = first; i &lt; last + 1; i++) {
            const $span = presenter.getSpanByNumber(i);
            if ($span.hasClass('selectable')) {
                $span.toggleClass('selected');
            }
        }
    };

    presenter.getNumberAttribute = function (et) {
        const elementNumber = parseInt($(et).attr('number'), 10);
        const parentNumber = parseInt($(et).closest('span.selectable').attr('number'), 10);

        return isNaN(elementNumber) ? parentNumber : elementNumber;
    }

    presenter.startSelection = function (et) {
        first = presenter.getNumberAttribute(et);
    };

    presenter.endSelection = function (et) {
        if (isNaN(first)) {
            first = 0;
            return false;
        }

        let last = presenter.getNumberAttribute(et);
        const isMultipleSelected = first !== last;

        if ($(et).hasClass('text_selection')) {
            last = first;
        }

        if (isMultipleSelected) {
            presenter.handleMultipleSelection(last);
        } else if (!beforeActive) {
            presenter.handleSingleSelection();
        }

        first = 0;
        beforeActive = false;
        if (window.getSelection) {
            window.getSelection().removeAllRanges();
        } else if (document.selection) {
            document.selection.empty();
        }
    };

    function isCorrect($element) {
        var number = parseInt($($element).attr('number'), 10),
            isInCorrectArray = $.inArray(number, presenter.markers.markedCorrect) &gt;= 0;
        return $element.hasClass('selected') ? isInCorrectArray : !isInCorrectArray;
    }

    presenter.turnOnEventListeners = function () {
        var $text_selection = presenter.$view.find('.text_selection');
        if (presenter.configuration.isTabindexEnabled) {$text_selection.attr('tabindex', '0');}

        if (MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            if (presenter.configuration.enableScroll) {

                var posDiff = 0;
                var lastScreenPos = {X:0, Y:0};
                $text_selection.on('touchstart', function (e) {
                    e.stopPropagation();
                    posDiff = 0;
                    var temp = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];
                    lastScreenPos.X = temp.screenX;
                    lastScreenPos.Y = temp.screenY;
                });

                $text_selection.on('touchend', function (e) {
                    e.stopPropagation();
                    if (posDiff&lt;15) {
                        presenter.startSelection(e.target);
                        presenter.endSelection(e.target);
                        presenter.configuration.isExerciseStarted = true;
                     }
                });

                $text_selection.on('touchmove', function (e) {
                    e.stopPropagation();
                    var temp = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];
                    posDiff += Math.abs(lastScreenPos.X - temp.screenX) + Math.abs(lastScreenPos.Y - temp.screenY);
                    lastScreenPos.X = temp.screenX;
                    lastScreenPos.Y = temp.screenY;
                });
            } else {
                $text_selection.on('touchstart', function (e) {
                    e.stopPropagation();
                    e.preventDefault();
                    presenter.startSelection(e.target);
                });

                $text_selection.on('touchend', function (e) {
                    e.stopPropagation();
                    presenter.configuration.isExerciseStarted = true;
                    e.preventDefault();
                    if (lastMoveEvent != null) {
                        presenter.endSelection(lastMoveEvent);
                    } else {
                        presenter.endSelection(e.target);
                    }
                    lastMoveEvent = null;
                });

                $text_selection.on('touchmove', function (e) {
                    e.stopPropagation();
                   e.preventDefault();
                    var temp = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];

                    lastMoveEvent = $(document.elementFromPoint(temp.pageX - $(document).scrollLeft(), temp.pageY - $(document).scrollTop()));
                });
            }
        } else {
            $text_selection.on('mouseup', function (e) {
                e.stopPropagation();
                presenter.configuration.isExerciseStarted = true;
                presenter.endSelection(e.target);
            });

            $text_selection.on('mousedown', function (e) {
                e.stopPropagation();
                presenter.startSelection(e.target);
            });

            $text_selection.find('.selectable').hover(
                function () {
                    $(this).addClass("hover");
                },
                function () {
                    $(this).removeClass("hover");
                }
            );
        }

        $text_selection.on('click', function (e) {
            e.stopPropagation();
        });

        presenter.configuration.areEventListenersOn = true;
    };

    presenter.turnOffEventListeners = function () {
        var $text_selection = presenter.$view.find('.text_selection'),
            selectable = $text_selection.find('.selectable');

        $text_selection.off();
        selectable.off();

        presenter.configuration.areEventListenersOn = false;
    };

    presenter.turnOnShowAnswersListeners = function () {
        presenter.eventBus.addEventListener('ShowAnswers', this);
        presenter.eventBus.addEventListener('HideAnswers', this);
        presenter.eventBus.addEventListener("GradualShowAnswers", this);
        presenter.eventBus.addEventListener("GradualHideAnswers", this);
    };

    function getSpace(i) {
        return "&lt;span left=\"" + i + "\" right=\"" + (i + 1) + "\"&gt; &lt;/span&gt;";
    }

    function getSpecialIfStarted(word) {
        return isLastSpecialSign(word) &amp;&amp; (presenter.isStartedWrong(word) || presenter.isStartedCorrect(word)) ? word[word.length - 1] : "";
    }

    presenter.upgradeModel = function(model) {
        var upgradedModel = upgradeModelEnableScrollProperty(model);
        upgradedModel = upgradeLangTag(upgradedModel);
        return presenter.upgradeSpeechTexts(upgradedModel);
    };

    function upgradeModelEnableScrollProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['enableScroll']){
            upgradedModel['enableScroll'] = false;
        }

        return upgradedModel;
    }

    function upgradeLangTag(model) {
        let upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = "";
        }

        return upgradedModel;
    }

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["selectedSectionStart"]) {
            upgradedModel["speechTexts"]["selectedSectionStart"]
              = {selectedSectionStart: ""};
        }
        if (!upgradedModel["speechTexts"]["selectedSectionEnd"]) {
            upgradedModel["speechTexts"]["selectedSectionEnd"]
              = {selectedSectionEnd: ""};
        }
        if (!upgradedModel["speechTexts"]["selected"]) {
            upgradedModel["speechTexts"]["selected"]
              = {selected: ""};
        }
        if (!upgradedModel["speechTexts"]["deselected"]) {
            upgradedModel["speechTexts"]["deselected"]
              = {deselected: ""};
        }
        if (!upgradedModel["speechTexts"]["wrong"]) {
            upgradedModel["speechTexts"]["wrong"]
              = {wrong: ""};
        }
        if (!upgradedModel["speechTexts"]["correct"]) {
            upgradedModel["speechTexts"]["correct"]
              = {correct: ""};
        }
        if (!upgradedModel["speechTexts"]["phrase"]) {
            upgradedModel["speechTexts"]["phrase"]
              = {phrase: ""};
        }
        if (!upgradedModel["speechTexts"]["phraseEnd"]) {
            upgradedModel["speechTexts"]["phraseEnd"]
              = {phraseEnd: ""};
        }

        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            selectedSectionStart: DEFAULT_TTS_PHRASES.selectedSectionStart,
            selectedSectionEnd: DEFAULT_TTS_PHRASES.selectedSectionEnd,
            selected: DEFAULT_TTS_PHRASES.selected,
            deselected: DEFAULT_TTS_PHRASES.deselected,
            wrong: DEFAULT_TTS_PHRASES.wrong,
            correct: DEFAULT_TTS_PHRASES.correct,
            phrase: DEFAULT_TTS_PHRASES.phrase,
            phraseEnd: DEFAULT_TTS_PHRASES.phraseEnd
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            selectedSectionStart: getSpeechTextProperty(
                speechTexts.selectedSectionStart.selectedSectionStart,
                presenter.speechTexts.selectedSectionStart),
            selectedSectionEnd: getSpeechTextProperty(
                speechTexts.selectedSectionEnd.selectedSectionEnd,
                presenter.speechTexts.selectedSectionEnd),
            correct: getSpeechTextProperty(
                speechTexts.correct.correct,
                presenter.speechTexts.correct),
            wrong: getSpeechTextProperty(
                speechTexts.wrong.wrong,
                presenter.speechTexts.wrong),
            selected: getSpeechTextProperty(
                speechTexts.selected.selected,
                presenter.speechTexts.selected),
            deselected: getSpeechTextProperty(
                speechTexts.deselected.deselected,
                presenter.speechTexts.deselected),
            phrase: getSpeechTextProperty(
                speechTexts.phrase.phrase,
                presenter.speechTexts.phrase),
            phraseEnd: getSpeechTextProperty(
                speechTexts.phraseEnd.phraseEnd,
                presenter.speechTexts.phraseEnd)
        };
    };

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        if (isPreview) {
            presenter.$view.append($(presenter.configuration.renderedPreview));
        } else {
            presenter.$view.append($(presenter.configuration.renderedRun));
            presenter.setVisibility(presenter.configuration.isVisible);
        }
    };

    presenter.ERROR_CODES = {
        M01: 'Text cannot be empty',
        M02: 'Text cannot be w/o \\correct{} or \\wrong{}',
        M03: 'You cannot use \\wrong{} in "All selectable" mode',
        M04: 'Empty word in marker',
        M05: 'In single selection you have to mark only one phrase as correct and at least one mark as wrong',
        M06: '\\alt{} cannot contain \\correct{} or \\wrong{}',
        M07: 'When in All selectable mode \\alt{} visible text section must contain only a single word'
    };

    presenter.MODE = {
        'Mark phrases to select': 'MARK_PHRASES',
        'All selectable': 'ALL_SELECTABLE',
        DEFAULT: 'Mark phrases to select'
    };

    presenter.SELECTION_TYPE = {
        'Single select': 'SINGLESELECT',
        'Multiselect': 'MULTISELECT',
        DEFAULT: 'Single select'
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
        presenter.turnOnEventListeners();
        presenter.turnOnShowAnswersListeners();
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    function getErrorObject(ec) {
        return {isValid: false, errorCode: ec};
    }

    presenter.validateModel = function (model) {
        presenter.setSpeechTexts(model['speechTexts']);

        if (ModelValidationUtils.isStringEmpty(model.Text)) {
            return getErrorObject('M01');
        }

        if (!presenter.vaildateTagsInAltText(model.Text)){
            return getErrorObject('M06');
        }

        const isTabindexEnabled = ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']);
        const mode = ModelValidationUtils.validateOption(presenter.MODE, model.Mode);
        const selectionType = ModelValidationUtils.validateOption(presenter.SELECTION_TYPE, model['Selection type']);
        const isLettersSelectionEnabled = ModelValidationUtils.validateBoolean(model['Enable letters selections']);

        if (mode == "ALL_SELECTABLE" &amp;&amp; !presenter.validateSingleWordAltText(model.Text)) {
            return getErrorObject('M07');
        }

        presenter.areAllPhrasesSingleWord = !presenter.detectMultipleWordPhrases(model.Text);

        let parsedText = presenter.textParser.parseAltTexts(model.Text);
        if (isLettersSelectionEnabled) {
            parsedText = presenter.parseCharacters(parsedText, mode, selectionType);
        } else {
            parsedText = presenter.parseWords(parsedText, mode, selectionType);
        }

        if (!parsedText.isValid) {
            return getErrorObject(parsedText.errorCode);
        }

        return {
            isValid: true,
            mode: mode,
            selection_type: selectionType,
            renderedRun: parsedText.renderedRun,
            renderedPreview: parsedText.renderedPreview,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isExerciseStarted: false,
            areEventListenersOn: true,
            addonID: model['ID'],
            isActivity: !(ModelValidationUtils.validateBoolean(model['isNotActivity'])),
            isTabindexEnabled: isTabindexEnabled,
            enableScroll: ModelValidationUtils.validateBoolean(model['enableScroll']),
            langTag: model['langAttribute']
        };
    };

    presenter.getMarked = function (wrong, correct) {
        return {
            markedWrong: wrong,
            markedCorrect: correct
        };
    };

    presenter.connectWords = function (words) {
        var i, j,
            longWord = '',
            result = [];

        for (i = 0; i &lt; words.length; i++) {
            if ((presenter.isStartedCorrect(words[i]) || presenter.isStartedWrong(words[i])) &amp;&amp; !presenter.isMarkedCorrect(words[i]) &amp;&amp; !presenter.isMarkedWrong(words[i])) {
                if (presenter.isStartedCorrect(words[i])) {
                    longWord += words[i] + ' ';
                    for (j = i + 1; j &lt; words.length; j++) {
                        if (presenter.hasClosingBracket(words[j])) {
                            longWord += words[j];
                            i = j;
                            j = words.length + 1;
                        } else {
                            longWord += words[j] + ' ';
                        }
                    }
                    result.push(longWord);
                    longWord = '';
                } else if (presenter.isStartedWrong(words[i])) {
                    longWord += words[i] + ' ';
                    for (j = i + 1; j &lt; words.length; j++) {
                        if (presenter.hasClosingBracket(words[j])) {
                            longWord += words[j];
                            i = j;
                            j = words.length + 1;
                        } else {
                            longWord += words[j] + ' ';
                        }
                    }
                    result.push(longWord);
                    longWord = '';
                }
            } else {
                result.push(words[i]);
            }
        }

        return result;
    };

    presenter.parseCharacters = function (text, mode, selection_type) {
        var i,
            result = '',
            words = [],
            markedCorrect = [],
            markedWrong = [],
            renderedPreview = '',
            renderedRun = '',
            amountWrong,
            amountCorrect,
            isTagClosed = true,
            spanNumber = 0,
            tmpWord = '',
            wrongMarkerInAllSelectable = false,
            emptyWord = false,
            stack = 0,
            counted = null;

        text = presenter.removeNonBreakingSpacesInWith(text, ' ');

        HTMLParser(text.replace(/&amp;nbsp;/g, ' '), {
            start: function (tag, attrs, unary) {
                renderedPreview += "&lt;" + tag;
                renderedRun += "&lt;" + tag;

                for (i = 0; i &lt; attrs.length; i++) {
                    renderedPreview += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
                    renderedRun += " " + attrs[i].name + '="' + attrs[i].escaped + '"';
                }

                renderedPreview += (unary ? "/" : "") + "&gt;";
                renderedRun += (unary ? "/" : "") + "&gt;";
            },
            end: function (tag) {
                renderedPreview += "&lt;/" + tag + "&gt;";
                renderedRun += "&lt;/" + tag + "&gt;";
            },
            chars: function (text) {
                //words = text.match(/\\?[\sa-zA-Z0-9\.\,]+(?:\{[\sa-zA-Z0-9]+\})?/g);
                words = text.split("\\");
                for (var j = 0; j &lt; words.length; j++) {
                    if (words[0] == "") {
                        words.splice(0, 1);
                    }
                }
                for (var i = 0; i &lt; words.length; i++) {
                    if (words[0] == "") {
                        words.splice(0, 1);
                    }
                    if (words[i].indexOf("correct{") &gt; -1 || words[i].indexOf("wrong{") &gt; -1) {
                        words[i] = '\\' + words[i];
                    }
                }

                for (i = 0; i &lt; words.length; i++) {
                    if (isTagClosed === true) {
                        if (words[i] === ' ') {
                            renderedPreview += getSpace(spanNumber);
                            renderedRun += getSpace(spanNumber);
                        } else if (presenter.isMarkedCorrect(words[i])) {
                            tmpWord = presenter.cutMarkedCorrect(words[i]);

                            counted = presenter.countBrackets(words[i]);
                            if (counted.open &gt; counted.close) {
                                renderedPreview += '&lt;span class="correct selectable"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                                renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                                isTagClosed = false;
                                markedCorrect.push(spanNumber);
                                spanNumber++;
                            } else {
                                $.each(presenter.getCorrectWords(words[i]), function (index, word) {
                                    if (word.length == 0) {
                                        if (index == 1) {
                                            markedCorrect.push(spanNumber);
                                        }
                                        return;
                                    }
                                    var selectable = index == 1 ? 'selectable' : '',
                                        correct = index == 1 ? 'correct' : '';
                                    renderedPreview += '&lt;span class="' + correct + ' ' + selectable + '"&gt;' + word + '&lt;/span&gt;';
                                    if (mode === 'ALL_SELECTABLE') {
                                        renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + word + '&lt;/span&gt;';
                                        markedWrong.push(spanNumber);
                                    } else {
                                        renderedRun += '&lt;span class="' + selectable + '" number="' + spanNumber + '"&gt;' + word + '&lt;/span&gt;';
                                    }
                                    if (index == 1) {
                                        markedCorrect.push(spanNumber);
                                    }
                                    spanNumber++;
                                });
                            }

                            if (ModelValidationUtils.isStringEmpty(tmpWord)) {
                                emptyWord = true;
                            }
                        } else if (presenter.isMarkedWrong(words[i])) {
                            tmpWord = presenter.cutMarkedWrong(words[i]);

                            counted = presenter.countBrackets(words[i]);
                            if (counted.open &gt; counted.close) {
                                renderedPreview += '&lt;span class="wrong selectable"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                                renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                                isTagClosed = false;
                                markedWrong.push(spanNumber);
                                spanNumber++;
                            } else {
                                $.each(presenter.getWrongWords(words[i]), function (index, word) {
                                    if (word.length == 0) {
                                        if (index == 1) {
                                            markedWrong.push(spanNumber);
                                        }
                                        return;
                                    }
                                    var selectable = index == 1 ? 'selectable' : '',
                                        wrong = index == 1 ? 'wrong' : '';
                                    renderedPreview += '&lt;span class="' + wrong + ' ' + selectable + '"&gt;' + word + '&lt;/span&gt;';
                                    renderedRun += '&lt;span class="' + selectable + '" number="' + spanNumber + '"&gt;' + word + '&lt;/span&gt;';
                                    if (index == 1) {
                                        markedWrong.push(spanNumber);
                                    }
                                    spanNumber++;
                                });
                            }

                            if (ModelValidationUtils.isStringEmpty(tmpWord)) {
                                emptyWord = true;
                            }
                            if (mode === 'ALL_SELECTABLE') {
                                wrongMarkerInAllSelectable = true;
                            }
                        } else if (presenter.isStartedCorrect(words[i])) {
                            tmpWord = presenter.cutMarkedCorrect(words[i]);

                            counted = presenter.countBrackets(words[i]);
                            stack += counted.open;
                            stack -= counted.close;

                            renderedPreview += '&lt;span class="correct selectable"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                            renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                            markedCorrect.push(spanNumber);
                            spanNumber++;
                            isTagClosed = false;
                        } else if (presenter.isStartedWrong(words[i])) {
                            tmpWord = presenter.cutMarkedWrong(words[i]);

                            counted = presenter.countBrackets(words[i]);
                            stack += counted.open;
                            stack -= counted.close;

                            renderedPreview += '&lt;span class="wrong selectable"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                            renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + tmpWord + getSpecialIfStarted(words[i]);
                            markedWrong.push(spanNumber);
                            spanNumber++;
                            isTagClosed = false;
                        } else {
                            counted = presenter.countBrackets(words[i]);
                            stack += counted.open;
                            stack -= counted.close;

                            if (mode === 'ALL_SELECTABLE') {
                                renderedRun += '&lt;span class="selectable" number="' + spanNumber + '"&gt;' + words[i] + '&lt;/span&gt;';
                                markedWrong.push(spanNumber);
                            } else if (mode === 'MARK_PHRASES') {
                                renderedRun += '&lt;span number="' + spanNumber + '"&gt;' + words[i] + '&lt;/span&gt;';
                            }
                            renderedPreview += '&lt;span number="' + spanNumber + '"&gt;' + words[i] + '&lt;/span&gt;';

                            spanNumber++;
                        }
                    } else { // isTagClosed === false
                        counted = presenter.countBrackets(words[i]);
                        if (counted.open === counted.close) {
                            renderedPreview += words[i] + ' ';
                            renderedRun += words[i] + ' ';
                        } else {
                            if (counted.close &gt;= (stack + counted.open)) {
                                tmpWord = presenter.cutLastClosingBracket(words[i]);
                            } else {
                                tmpWord = words[i];
                            }
                            stack += counted.open;
                            stack -= counted.close;
                            if (stack === 0) {
                                renderedPreview += tmpWord + '&lt;/span&gt;';
                                renderedRun += tmpWord + '&lt;/span&gt;';
                                isTagClosed = true;
                            } else {
                                renderedPreview += tmpWord + ' ';
                                renderedRun += tmpWord + ' ';
                            }
                        }
                    }
                }
                text = words.join(' ');
                result += text;
            },
            comment: function (text) {
            }
        });

        amountCorrect = markedCorrect.length;
        amountWrong = markedWrong.length;

        presenter.markers = presenter.getMarked(markedWrong, markedCorrect);

        if (amountCorrect === 0 &amp;&amp; amountWrong === 0) {
            return getErrorObject('M02');
        }

        if (wrongMarkerInAllSelectable) {
            return getErrorObject('M03');
        }

        if (emptyWord) {
            return getErrorObject('M04');
        }

        if ((amountCorrect !== 1 || amountWrong &lt; 1) &amp;&amp; selection_type === 'SINGLESELECT') { // HasOneCorrectAtLeastOneWrongInSingleSelectionTypeSelection
            return getErrorObject('M05');
        }

        renderedRun = presenter.deselectSpansWithOnlySpaces(renderedRun);

        return {
            isValid: true,
            renderedPreview: '&lt;div class="text_selection"&gt;' + renderedPreview + '&lt;/div&gt;',
            renderedRun: '&lt;div class="text_selection"&gt;' + renderedRun + '&lt;/div&gt;',
            markedWrong: markedWrong,
            markedCorrect: markedCorrect
        };
    };

    presenter.deselectSpansWithOnlySpaces = function (htmlString) {
        var $text = $(htmlString);

        var textWithDeselectedSpaces = "";
        $text.each(function () {
            var $element = $(this);
            if ($element.hasClass("selectable")) {
                if ($element.text().trim() == "") {
                    $element.removeClass("selectable");
                }
            }
            textWithDeselectedSpaces += $(this).context.outerHTML;
        });

        return textWithDeselectedSpaces;
    };

    presenter.removeNonBreakingSpacesInWith = function (text, changeTo) {
        var textWithoutSpaces = "";

        while (true) {
            var nbspIndex = text.indexOf("&amp;nbsp;");
            if (nbspIndex === -1) {
                textWithoutSpaces += text;
                break;
            }

            textWithoutSpaces = textWithoutSpaces + text.slice(0, nbspIndex) + changeTo;
            text = text.slice(nbspIndex + 6, text.length);
        }

        return textWithoutSpaces;
    };

    presenter.markMathJax = function (text) {
        var findMathJaxRex = /\\\(.*?\\\)/;
        var match = findMathJaxRex.exec(text);
        while (match !== null) {
            text = text.replace(match[0], MATH_JAX_MARKER + presenter.markedMathJaxContent.length);
            presenter.markedMathJaxContent.push(match[0]);
            match = findMathJaxRex.exec(text);
        }

        return text;
    };

    presenter.retrieveMathJax = function (text) {
        for (var i = 0; i &lt; presenter.markedMathJaxContent.length; i++) {
            text = text.replace(MATH_JAX_MARKER + i, presenter.markedMathJaxContent[i]);
        }
        return text;
    };

    function wrapText(text, classes, number) {
        var $span = $('&lt;span&gt;&lt;/span&gt;');
        classes.forEach(function (c) {
            $span.addClass(c);
        });
        if (number !== undefined) {
            $span.attr('number', number);
        }
        $span.html(text);

        return $span[0].outerHTML;
    }

    function wrapSpaces(spaces, num) {
        var $span = $('&lt;span&gt;&lt;/span&gt;');
        $span.attr('left', num);
        $span.attr('right', num + 1);
        $span.html(spaces);

        return $span[0].outerHTML;
    }

    presenter.parseWords = function (text, mode, selection_type) {
        text = presenter.markMathJax(text.replace(/&amp;nbsp;/g, ' '));
        var previewHTML = '', runHTML = '';
        var spanIndex = 0;
        var spansMarkedCorrect = [], spansMarkedWrong = [];
        var isEmptyWord = false;
        var isWrongMarker = false;

        var correct = /\\correct{([^}]+)?}/.source;
        var wrong = /\\wrong{([^}]+)?}/.source;
        var tags = /(&lt;[^&gt;]+?&gt;)/.source;
        var word = /([^\s\.,#!$%\^\*:{}=\-_`~\(\)&lt;&gt;]+)/.source;
        var whiteSpaces = /([\s\.,#!$%\^\*:{}=\-_`~\(\)]+)/.source;

        var mainRex = new RegExp([correct, wrong, tags, word, whiteSpaces].join('|'), 'g');

        var match = mainRex.exec(text);
        while (match !== null) {
            if (match[1]) { // correct
                if (match[1] === '') isEmptyWord = true;
                previewHTML += wrapText(match[1], ['correct', 'selectable']);
                runHTML += wrapText(match[1], ['selectable'], spanIndex);
                spansMarkedCorrect.push(spanIndex);
                spanIndex++;
            } else if (match[2]) { // wrong
                if (match[2] === '') isEmptyWord = true;
                isWrongMarker = true;
                previewHTML += wrapText(match[2], ['wrong', 'selectable']);
                runHTML += wrapText(match[2], ['selectable'], spanIndex);
                spansMarkedWrong.push(spanIndex);
                spanIndex++;
            } else if (match[3]) { // HTML tag
                previewHTML += match[3];
                runHTML += match[3];
            } else if (match[4]) { // words
                if (mode === 'ALL_SELECTABLE') { // word
                    previewHTML += match[4];
                    runHTML += wrapText(match[4], ['selectable'], spanIndex);
                    spansMarkedWrong.push(spanIndex);
                } else {
                    previewHTML += match[4];
                    runHTML += wrapText(match[4], [], spanIndex);
                }
                spanIndex++;
            } else { // spaces
                previewHTML += match[5];
                runHTML += wrapSpaces(match[5], spanIndex - 1);
            }

            // get next match
            match = mainRex.exec(text);
        }

        presenter.markers = {
            markedCorrect: spansMarkedCorrect,
            markedWrong: spansMarkedWrong
        };

        if (spansMarkedCorrect.length === 0 &amp;&amp; spansMarkedWrong.length === 0) {
            return getErrorObject('M02');
        }

        if (mode === 'ALL_SELECTABLE' &amp;&amp; isWrongMarker) {
            return getErrorObject('M03');
        }

        if (isEmptyWord) {
            return getErrorObject('M04');
        }

        if ((spansMarkedCorrect.length !== 1 || spansMarkedWrong.length &lt; 1) &amp;&amp; selection_type === 'SINGLESELECT') {
            return getErrorObject('M05');
        }

        return {
            isValid: true,
            renderedPreview: '&lt;div class="text_selection"&gt;' + presenter.retrieveMathJax(previewHTML) + '&lt;/div&gt;',
            renderedRun: '&lt;div class="text_selection"&gt;' + presenter.retrieveMathJax(runHTML) + '&lt;/div&gt;'
        };

    };

    presenter.executeCommand = function (name, params) {
        if (!presenter.configuration.isValid) return;

        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'isAllOK': presenter.isAllOK,
            'isAttempted': presenter.isAttempted
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        if (presenter.isShowAnswers) {
            presenter.hideAnswers();
        }

        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function () {
        if (presenter.isShowAnswers) {
            presenter.hideAnswers();
        }

        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.reset = function () {
        if (presenter.isShowAnswers) {
            presenter.hideAnswers();
        }

        presenter.activeGradualShowAnswersItems = [];
        presenter.selected_elements = null;

        presenter.$view.find('.text_selection').find('.selected').removeClass('selected');
        presenter.setWorkMode();
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
    };

    presenter.getState = function () {
        let returnToShowAnswers = false;
        let returnToGradualShowAnswers = false;
        if (presenter.isShowAnswers) {
            presenter.hideAnswers();
            returnToShowAnswers = true;
        } else if (presenter.isGradualShowAnswersActive) {
            presenter.gradualHideAnswers();
            returnToGradualShowAnswers = true;
        }

        const allSelected = presenter.$view.find('.text_selection').find('.selected');
        const numberSelected = [];

        for (let i = 0; i &lt; allSelected.length; i++) {
            numberSelected.push($(allSelected[i]).attr('number'));
        }

        if (returnToShowAnswers) presenter.showAnswers();
        if (returnToGradualShowAnswers) presenter.restoreGradualShowAnswers();

        return JSON.stringify({
            numbers: numberSelected,
            isVisible: presenter.configuration.isVisible,
            isExerciseStarted: presenter.configuration.isExerciseStarted
        });
    };

    presenter.setState = function (state) {
        if (ModelValidationUtils.isStringEmpty(state)) return;

        var parsed = JSON.parse(state),
            nums = parsed.numbers,
            isVisible = parsed.isVisible,
            isExerciseStarted = parsed.isExerciseStarted;

        for (var i = 0; i &lt; nums.length; i++) {
            presenter.$view.find('.text_selection').find("span[number='" + nums[i] + "']").addClass("selected");
        }

        if (isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }

        presenter.configuration.isVisible = isVisible;
        presenter.configuration.isExerciseStarted = isExerciseStarted;
    };

    function intersection(a, b) {
        return a.filter(function (i) {
            return b.indexOf(parseInt(i, 10)) !== -1;
        });
    }

    presenter.setShowErrorsMode = function () {
        if (presenter.configuration.isActivity) {
            if (presenter.isShowAnswers) {
                presenter.hideAnswers();
            }

            presenter.isWorkMode = false;

            presenter.turnOffEventListeners();
            addDisabledClass();

            if (!presenter.configuration.isExerciseStarted) return false;

            if (presenter.isShowAnswers) presenter.hideAnswers();

            var i;

            var numbersSelected = presenter.$view.find('.text_selection').find('.selected').map(function () {
                return this.getAttribute('number');
            }).get();

            var numbersCorrect = presenter.markers.markedCorrect;
            var numbersWrong = presenter.markers.markedWrong;

            var correctSelected = intersection(numbersSelected, numbersCorrect);

            for (i = 0; i &lt; correctSelected.length; i++) {
                presenter.$view.find('.text_selection').find("span[number='" + correctSelected[i] + "']").addClass('correct');
            }

            var selectedWrong = intersection(numbersSelected, numbersWrong);
            for (i = 0; i &lt; selectedWrong.length; i++) {
                presenter.$view.find('.text_selection').find("span[number='" + selectedWrong[i] + "']").addClass('wrong');
            }
        }
    };

    presenter.setWorkMode = function () {
        if (presenter.configuration.isActivity) {
            if (presenter.isShowAnswers) {
                presenter.hideAnswers();
            }

            presenter.isWorkMode = true;

            presenter.$view.find('.text_selection').find('.correct').removeClass('correct');
            presenter.$view.find('.text_selection').find('.wrong').removeClass('wrong');

            presenter.selected_elements = presenter.$view.find('.text_selection').find('.selected');
            presenter.turnOffEventListeners();
            presenter.turnOnEventListeners();
            removeDisabledClass();
        }
    };

    function points(selector) {
        var $selectedElements = null;
        if (!presenter.isShowAnswers) {
            $selectedElements = presenter.$view.find('.text_selection').find('.selected');
        } else {
            if (presenter.selected_elements) {
                $selectedElements = presenter.selected_elements;
            } else {
                return 0;
            }
        }
        var numbersSelected = $selectedElements.map(function () {
            return parseInt(this.getAttribute('number'), 10);
        }).get();

        return intersection(selector, numbersSelected).length;
    }

    presenter.getErrorCount = function () {
        if (presenter.configuration.isActivity) {
            return points(presenter.markers.markedWrong);
        } else {
            return 0;
        }
    };

    presenter.getMaxScore = function () {
        if (presenter.configuration.isActivity) {
            return presenter.markers.markedCorrect.length;
        } else {
            return 0;
        }
    };

    presenter.getScore = function () {
        if (presenter.configuration.isActivity) {
            return points(presenter.markers.markedCorrect);
        } else {
            return 0;
        }
    };

    presenter.isAllOK = function () {
        var isMaxScore = presenter.getMaxScore() == presenter.getScore();
        var hasErrors = presenter.getErrorCount() &gt; 0;

        return isMaxScore &amp;&amp; !hasErrors;
    };

    presenter.isAttempted = function () {
        if (presenter.isShowAnswers) {
            presenter.hideAnswers();
        }

        return presenter.$view.find('.text_selection').find('.selected').length &gt; 0;
    };

    presenter.showAnswers = function () {
        if (!presenter.configuration.isActivity) {
            return;
        }

        if (presenter.isShowAnswers) {
            return false;
        }

        if (!presenter.isWorkMode) {
            presenter.setWorkMode();
        }

        presenter.turnOffEventListeners();
        addDisabledClass();

        presenter.isShowAnswers = true;
        presenter.saveAndRemoveSelection();

        var allSelectable = presenter.$view.find(".selectable");
        presenter.showAnswersForElements(allSelectable.length);
    };

    presenter.showAnswersForElements = function (count) {
        for (var i = 0; i &lt; count; i++) {
            var elem = presenter.$view.find(".selectable")[i];
            var elem_number = parseInt($(elem).attr("number"), 10);

            if (presenter.markers.markedCorrect.indexOf(elem_number) !== -1) {
                $(elem).addClass("correct-answer");
            }
        }
    }

    presenter.showCorrectAnswer = function (item) {
        var elementNumber = presenter.markers.markedCorrect[item];
        var $elem = presenter.$view.find(".selectable[number='" + elementNumber + "']");
        $elem.addClass("correct-answer");
    }

    presenter.hideAnswers = function () {
        if (!presenter.configuration.isActivity) {
            return;
        }

        if (!presenter.isShowAnswers) {
            return false;
        }

        presenter.turnOnEventListeners();
        removeDisabledClass();

        presenter.activeGradualShowAnswersItems = [];
        presenter.isShowAnswers = false;
        presenter.restoreSelection();
    };

    presenter.saveAndRemoveSelection = function () {
        presenter.selected_elements = presenter.$view.find(".selected");
        presenter.selected_elements.removeClass("selected");
    }

    presenter.restoreSelection = function () {
        presenter.$view.find(".correct-answer").removeClass("correct-answer");
        if (presenter.selected_elements != null) presenter.selected_elements.addClass("selected");
    }

    presenter.onEventReceived = function (eventName, data) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        } else if (eventName === "GradualShowAnswers") {
            if (presenter.configuration.areEventListenersOn) {
                presenter.turnOffEventListeners();
                addDisabledClass();
            }
            if (data.moduleID === presenter.configuration.addonID) {
                presenter.gradualShowAnswers(parseInt(data.item, 10));
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.activeGradualShowAnswersItems = [];
            presenter.gradualHideAnswers();
        }
    };

    var TextSelectionKeyboardController = function (elements, elementsCount) {
        KeyboardController.call(this, elements, elementsCount);
        presenter._firstElementSwitch = true;
    };

    TextSelectionKeyboardController.prototype = Object.create(KeyboardController.prototype);

    TextSelectionKeyboardController.prototype.select = function (event) {
        if (event) {
            event.preventDefault();
        }
        if( presenter.isWorkMode &amp;&amp; !presenter.isShowAnswers ) {
            presenter.startSelection(this.getTarget(this.keyboardNavigationCurrentElement, true));
            presenter.endSelection(this.getTarget(this.keyboardNavigationCurrentElement, true));
            presenter.configuration.isExerciseStarted = true;
            presenter.readSelection(this.getTarget(this.keyboardNavigationCurrentElement, true).hasClass('selected'));
        }
    };

    TextSelectionKeyboardController.prototype.switchElement = function (move) {
        if (presenter._firstElementSwitch) {
            presenter._firstElementSwitch = false;
            this.markCurrentElement(0);
        } else {
            var new_position_index = this.keyboardNavigationCurrentElementIndex + move;
            if (new_position_index &lt; this.keyboardNavigationElementsLen &amp;&amp; new_position_index &gt;= 0) {
                KeyboardController.prototype.switchElement.call(this, move);
            }
        }
        presenter.readActiveElement(this.keyboardNavigationElements[this.keyboardNavigationCurrentElementIndex]);
    };

    TextSelectionKeyboardController.prototype.enter = function (event) {
        event.preventDefault();
        this.keyboardNavigationActive = true;
        presenter.readContent();
    };

     TextSelectionKeyboardController.prototype.escape = function (event) {
         presenter._firstElementSwitch = true;
         Object.getPrototypeOf(TextSelectionKeyboardController.prototype).escape.call(this);
     };

    presenter.buildKeyboardController = function () {
        var $text_selection = presenter.$view.find('.text_selection');
        var toSelect = $text_selection.find('.selectable');
        var jQueryToSelect = [];
        for (var i = 0; i &lt; toSelect.length; i++) {
            jQueryToSelect.push($(toSelect[i]));
        }

        presenter._keyboardController = new TextSelectionKeyboardController(jQueryToSelect, toSelect.length);
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        if (keycode === window.KeyboardControllerKeys.ESC) {
            event.preventDefault();
        }

        if (presenter._keyboardController === null) {
            presenter.buildKeyboardController();
        }

        presenter._keyboardController.handle(keycode, isShiftKeyDown, event);
    };

    presenter.readActiveElement = function($element) {
        if (!$element) {
            $element = presenter.$view.find('.keyboard_navigation_active_element');
        }

        if ($element.length === 0) return;

        var textVoices = presenter.getElementTextVoices($element);
        speak(textVoices);
    };

    presenter.getElementTextVoices = function($element) {
        var textVoices = [];

        var readPhrases = presenter.configuration.mode != "ALL_SELECTABLE" &amp;&amp; !presenter.areAllPhrasesSingleWord;
        if (readPhrases) {
            addTextVoiceForSelectableSpans(textVoices, $element);
        }

        addTextVoiceForElement(textVoices, $element);

        if (readPhrases) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.phraseEnd);
        }

        if ($element.hasClass('selected')) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selected);
        }

        if ($element.hasClass('correct')) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.correct);
        } else if ($element.hasClass('wrong')) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.wrong);
        } else if ($element.hasClass('correct-answer')) {
            addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selected);
        }

        return textVoices;
    };

    function addTextVoiceForElement(textVoices, $element) {
        var contentText = '';
        var langTag = '';
        var $ariaLabel = $element.closest('.addon_Text_Selection span[aria-label]:has(span[aria-hidden="true"])');
        if ($ariaLabel.length &gt; 0) {
            contentText = $ariaLabel.attr('aria-label');
            langTag = $ariaLabel.attr('lang');
            if (!langTag) {
               langTag =  presenter.configuration.langTag;
            }
        } else {
            contentText = presenter.getTextFromElementWithAltTexts($element);
            langTag = presenter.configuration.langTag;
        }
        textVoices.push(window.TTSUtils.getTextVoiceObject(contentText, langTag));
    }

    function addTextVoiceForSelectableSpans(textVoices, $element) {
        var elementIndex = -1;

        var selectables = presenter.$view.find('.selectable');
        var selectablesSize = selectables.size();
        if (selectablesSize &gt; 0) {
            for (var i = 0; i &lt; selectablesSize; i++) {
                if ($(selectables[i]).is($element)) {
                    elementIndex = i + 1;
                }
            }
        }

        var phraseText = presenter.speechTexts.phrase;
        if (elementIndex &gt; 0) {
            phraseText += ' ' + elementIndex;
        }
        addTextVoiceWithLanguageFromLesson(textVoices, phraseText);
    }

    presenter.readSelection = function(selected) {
        var textVoices = [];
        var speechText = selected
            ? presenter.speechTexts.selected
            : presenter.speechTexts.deselected;
        addTextVoiceWithLanguageFromLesson(textVoices, speechText);
        speak(textVoices);
    };

    presenter.readContent = function() {
        var textVoices = [];
        if(presenter.configuration.mode == "ALL_SELECTABLE") {
            textVoices = presenter.getSectionsTextVoices(presenter.$view);
        } else if(presenter.areAllPhrasesSingleWord) {
            textVoices = presenter.getWordsTextVoices(presenter.$view);
        } else {
            textVoices = presenter.getPhrasesTextVoices(presenter.$view);
        }
        speak(textVoices);
    };

    presenter.getWordsTextVoices = function($element) {
        var sanitizedHTML = window.xssUtils.sanitize($element[0].outerHTML);
        var $sanitizedElement = $(sanitizedHTML);

        $($sanitizedElement).find('.selectable').each(function(){
            var $this = $(this);

            if ($this.hasClass('selected')) {
                $this.html(SPLIT + $this.html() + SPLIT + SELECTED + SPLIT);
            }
            if ($this.hasClass('correct-answer')) {
                $this.html(SPLIT + $this.html() + SPLIT + SELECTED + SPLIT);
            } else if ($this.hasClass('wrong')) {
                $this.html(SPLIT + $this.html() + SPLIT + WRONG + SPLIT);
            } else if ($this.hasClass('correct')) {
                $this.html(SPLIT + $this.html() + SPLIT + CORRECT + SPLIT);
            }
        });

        var textArray = presenter.getTextFromElementWithAltTexts($sanitizedElement).split(SPLIT);

        var textVoices = [];

        for(var i = 0; i &lt; textArray.length; i++) {
            if(textArray[i].trim().length == 0) continue;
            if (0 === textArray[i].localeCompare(SELECTED)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selected);
            } else if (0 === textArray[i].localeCompare(CORRECT)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.correct);
            } else if (0 === textArray[i].localeCompare(WRONG)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.wrong);
            } else {
                addTextVoiceWithLanguageFromPresenter(textVoices, textArray[i]);
            }
        }

        return textVoices;
    };

    presenter.getPhrasesTextVoices = function($element) {
        var sanitizedHTML = window.xssUtils.sanitize($element[0].outerHTML);
        var $sanitizedElement = $(sanitizedHTML);

        $sanitizedElement.find('.selectable').each(function(index){
            var $this = $(this);

            $this.html(SPLIT + PHRASE + ' ' + (index+1) + SPLIT + $this.html() + SPLIT + PHRASE_END + SPLIT );

            if ($this.hasClass('selected')) {
                $this.html($this.html() + SELECTED + SPLIT);
            }
            if ($this.hasClass('correct-answer')) {
                $this.html($this.html() + SELECTED + SPLIT);
            } else if ($this.hasClass('wrong')) {
                $this.html($this.html() + WRONG + SPLIT);
            } else if ($this.hasClass('correct')) {
                $this.html($this.html() + CORRECT + SPLIT);
            }
        });

        var textArray = presenter.getTextFromElementWithAltTexts($sanitizedElement).split(SPLIT);

        var textVoices = [];

        for(var i = 0; i &lt; textArray.length; i++) {
            if(textArray[i].trim().length == 0) continue;
            if (-1 !== textArray[i].indexOf(PHRASE)) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(textArray[i].replace(PHRASE, presenter.speechTexts.phrase)));
            } else if (0 === textArray[i].localeCompare(PHRASE_END)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.phraseEnd);
            } else if (0 === textArray[i].localeCompare(SELECTED)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selected);
            } else if (0 === textArray[i].localeCompare(CORRECT)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.correct);
            } else if (0 === textArray[i].localeCompare(WRONG)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.wrong);
            } else {
                addTextVoiceWithLanguageFromPresenter(textVoices, textArray[i]);
            }
        }

        return textVoices;
    };

    function setSectionWrappingText($el, start_text, end_text) {
        var $parent = $el.closest('.addon_Text_Selection  span[aria-label]:has(span[aria-hidden="true"])');
        if($parent!=null &amp;&amp; $parent.length &gt; 0){
          $el = $parent;
        }
        $el.before($('&lt;span&gt;' + start_text + '&lt;/span&gt;'));
        $el.after($('&lt;span&gt;' + end_text + '&lt;/span&gt;'));
    }

    presenter.getSectionsTextVoices = function($element) {
        var $clone = $element.clone();

        $clone.find('.selectable').each(function(){
            var $this = $(this);

            if ($this.hasClass('correct-answer')) {
                setSectionWrappingText($(this), SELECTED_SECTION_START, SELECTED_SECTION_END);
            } else if ($this.hasClass('wrong')) {
                setSectionWrappingText($(this), WRONG_SECTION_START, WRONG_SECTION_END);
            } else if ($this.hasClass('correct')) {
                setSectionWrappingText($(this), CORRECT_SECTION_START, CORRECT_SECTION_END);
            } else if ($this.hasClass('selected')) {
                setSectionWrappingText($(this), SELECTED_SECTION_START, SELECTED_SECTION_END);
            }
        });

        var contentText = presenter.getTextFromElementWithAltTexts($clone);

        contentText = replaceAll(contentText, SELECTED_SECTION_END + '([^\\w\\d]*)' + SELECTED_SECTION_START, '$1');
        contentText = replaceAll(contentText, CORRECT_SECTION_END + '([^\\w\\d]*)' + CORRECT_SECTION_START, '$1');
        contentText = replaceAll(contentText, WRONG_SECTION_END + '([^\\w\\d]*)' + WRONG_SECTION_START, '$1');

        contentText = replaceAll(contentText, SELECTED_SECTION_START, SPLIT + SELECTED_SECTION_START + SPLIT);
        contentText = replaceAll(contentText, SELECTED_SECTION_END, SPLIT + SELECTED_SECTION_END + SPLIT);
        contentText = replaceAll(contentText, CORRECT_SECTION_START, SPLIT + SELECTED_SECTION_START + SPLIT);
        contentText = replaceAll(contentText, CORRECT_SECTION_END, SPLIT + SELECTED_SECTION_END + SPLIT + CORRECT + SPLIT);
        contentText = replaceAll(contentText, WRONG_SECTION_START, SPLIT + SELECTED_SECTION_START + SPLIT);
        contentText = replaceAll(contentText, WRONG_SECTION_END, SPLIT + SELECTED_SECTION_END + SPLIT + WRONG + SPLIT);

        var textArray = contentText.split(SPLIT);

        var textVoices = [];


        for(var i = 0; i &lt; textArray.length; i++) {
            if(textArray[i].trim().length == 0) continue;
            if (0 === textArray[i].localeCompare(SELECTED_SECTION_START)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selectedSectionStart);
            } else if (0 === textArray[i].localeCompare(SELECTED_SECTION_END)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.selectedSectionEnd);
            } else if (0 === textArray[i].localeCompare(CORRECT)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.correct);
            } else if (0 === textArray[i].localeCompare(WRONG)) {
                addTextVoiceWithLanguageFromLesson(textVoices, presenter.speechTexts.wrong);
            } else {
                addTextVoiceWithLanguageFromPresenter(textVoices, textArray[i]);
            }
        }

        return textVoices;

    };

    presenter.getTextFromElementWithAltTexts = function($element) {
        /*
        * This method assumes, that the only things requiring parsing are alt text elements!
        * Image alt attributes are only partially handled
        * */
        var text = '';
        var textVoices = window.TTSUtils.getTextVoiceArrayFromElement($element,presenter.configuration.langTag);
        for (var i = 0; i &lt; textVoices.length; i++) {
            text += textVoices[i].text;
        }
        return text;
    };

    function replaceAll(source, search, replace) {
        return source.replace(new RegExp(search, 'g'), replace);
    }

    function addTextVoiceWithLanguageFromLesson(textVoices, message) {
        addTextVoice(textVoices, message, false);
    }

    function addTextVoiceWithLanguageFromPresenter(textVoices, message) {
        addTextVoice(textVoices, message, true);
    }

    function addTextVoice(textVoices, message, usePresenterLangTag = false) {
        if (usePresenterLangTag) {
            textVoices.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
        } else {
            textVoices.push(window.TTSUtils.getTextVoiceObject(message));
        }
    }

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
        if (!isOn) {
            presenter._firstElementSwitch = true;
        }
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    /*
    * Return false if detected a \wrong or \correct tag within alt text, true otherwise
    * */
    presenter.vaildateTagsInAltText = function(text) {
        var correctMatch = text.match(/\\alt{[^}]*?\\correct.*?}/g) != null;
        var wrongMatch = text.match(/\\alt{[^}]*?\\wrong.*?}/g) != null;
        return !(correctMatch || wrongMatch);
    };

    /*
    * Return false if detected an alt text where visible text section contains more than one word, true otherwise
    * */
    presenter.validateSingleWordAltText = function(text) {
        var re = /\\alt{([^{}|]*?)\|[^{}|]*?}+/g; //Find all alt text elements
        do {
            var match = re.exec(text); //get the next match
            if(match &amp;&amp; !isSingleWord(match[1])) {
                return false;
            }
        } while (match);
        return true;
    };

    function isSingleWord(text) {
        return !/[\s,.]/.test(text.trim());
    }

    presenter.detectMultipleWordPhrases = function(text) {
        var parsedText = presenter.textParser.parseAltTexts(text);
        var regex_correct = /\\correct{([^}]*?)}/g;
        var regex_wrong = /\\wrong{([^}]*?)}/g;
        do {
            var match = regex_correct.exec(parsedText); //get the next match
            if(match &amp;&amp; !isSingleWord(match[1])){
                return true;
            }
        } while (match);
        do {
            var match = regex_wrong.exec(parsedText); //get the next match
            if(match &amp;&amp; !isSingleWord(match[1])){
                return true;
            }
        } while (match);
        return false;
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

   presenter.getPrintableHTML = function (model, showAnswers) {
       const upgradedModel = presenter.upgradeModel(model);
       const configuration = presenter.validateModel(upgradedModel);
       const $html = createHTMLStructureForPrintable(configuration);

       chosePrintableStateMode(showAnswers);
       upgradeHTMLStructureForPrintable($html, configuration.mode);
       presenter.printableStateMode = null;

       const $view = createViewForPrintable(upgradedModel, $html);
       return $view[0].outerHTML;
   };

   function createHTMLStructureForPrintable(configuration) {
       var $renderedRun = $(configuration.renderedRun).clone();
       if (presenter.printableState) {
           var numbers = presenter.printableState.numbers;
           for (var i = 0; i &lt; numbers.length; i++) {
               $renderedRun.find("span[number='" + numbers[i] + "']").addClass("selected");
           }
       }
       return $renderedRun;
   }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        } else {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function upgradeHTMLStructureForPrintable($html, mode) {
        switch (presenter.printableStateMode) {
            case presenter.PRINTABLE_STATE_MODE.EMPTY:
                upgradeHTMLForPrintableWhenEmptyPrintableStateMode($html);
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS:
                upgradeHTMLForPrintableWhenShowAnswersPrintableStateMode($html);
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS:
                upgradeHTMLForPrintableWhenShowUserAnswersPrintableStateMode($html);
                break;
            case presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS:
                upgradeHTMLForPrintableWhenCheckAnswersPrintableStateMode($html);
                break;
        }
        upgradeHTMLForPrintableElementsAccordingToMode($html, mode);
        cleanHTMLStructureFromNotPrintableAttributesAndClasses($html);
    }

    function upgradeHTMLForPrintableWhenEmptyPrintableStateMode($html) {}

    function upgradeHTMLForPrintableWhenShowAnswersPrintableStateMode($html) {
        var $spans = $(findSelectableElements($html));
        $spans.each(function() {
            var number = parseInt($(this).attr('number'), 10);
            var isInCorrectArray = $.inArray(number, presenter.markers.markedCorrect) &gt;= 0;
            if (isInCorrectArray)
                $(this).wrap(`&lt;u class="${CSS_CLASSES.PRINTABLE_CORRECT_ANSWER}"&gt;&lt;/u&gt;`);
        })
    }

    function upgradeHTMLForPrintableWhenShowUserAnswersPrintableStateMode($html) {
        var $spans = $(findSelectedElements($html));
        $spans.each(function() {
            var $wrapper = $('&lt;u&gt;&lt;/u&gt;');
            $wrapper.addClass(CSS_CLASSES.PRINTABLE_SELECTED);
            $(this).wrap($wrapper);
        })
    }

    function upgradeHTMLForPrintableWhenCheckAnswersPrintableStateMode($html) {
        var $spans = $(findSelectedElements($html));
        $spans.each(function() {
            var $span = $( this );
            var $wrapper = $('&lt;u&gt;&lt;/u&gt;');

            $wrapper.addClass(CSS_CLASSES.PRINTABLE_SELECTED);
            if (isCorrect($span)) {
                $wrapper.addClass(CSS_CLASSES.PRINTABLE_CORRECT);
            } else {
                $wrapper.addClass(CSS_CLASSES.PRINTABLE_WRONG);
            }
            $span.wrap($wrapper);
        })
    }

    function upgradeHTMLForPrintableElementsAccordingToMode($html, mode) {
        if (mode === 'MARK_PHRASES')
            upgradeHTMLForPrintableWhenMarkPhrasesToSelectMode($html);
    }

    function upgradeHTMLForPrintableWhenMarkPhrasesToSelectMode($html) {
        findSelectableElements($html).wrap('&lt;strong&gt;&lt;/strong&gt;');
    }

    function cleanHTMLStructureFromNotPrintableAttributesAndClasses($html) {
        findSelectableElements($html).removeClass(CSS_CLASSES.SELECTABLE);
        findSelectedElements($html).removeClass(CSS_CLASSES.SELECTED);
        findSpaceElements($html).contents().unwrap('&lt;span&gt;&lt;/span&gt;');
        findNumberElements($html).contents().unwrap('&lt;span&gt;&lt;/span&gt;');
    }

    function findSelectedElements($html) {
        return $html.find(`.${CSS_CLASSES.SELECTED}`);
    }

    function findSelectableElements($html) {
        return $html.find(`.${CSS_CLASSES.SELECTABLE}`);
    }

    function findSpaceElements($html) {
        return $html.find('span[left]');
    }

    function findNumberElements($html) {
        return $html.find('span[number]');
    }

    function createViewForPrintable(model, $html) {
        var $view = $("&lt;div&gt;&lt;/div&gt;");
        $view.attr("id", model.ID);
        $view.addClass(CSS_CLASSES.PRINTABLE);
        $view.css("max-width", model["Width"] + "px");
        $view.html($html.html());
        return $view;
    }

    presenter.getActivitiesCount = function () {
        return presenter.markers.markedCorrect.length;
    }

    presenter.isShowingAnswers = function () {
        return (presenter.configuration.isActivity &amp;&amp; presenter.isShowAnswers)
            || presenter.isGradualShowAnswersActive;
    }

    presenter.gradualShowAnswers = function (item) {
        if (!presenter.isGradualShowAnswersActive) {
            presenter.saveAndRemoveSelection();
            presenter.isGradualShowAnswersActive = true;
        }
        presenter.activeGradualShowAnswersItems.push(item);
        presenter.showCorrectAnswer(item);
    };

    presenter.restoreGradualShowAnswers = function () {
        if (!presenter.activeGradualShowAnswersItems.length) {
            return;
        }
        if (presenter.configuration.areEventListenersOn) {
            presenter.turnOffEventListeners();
            addDisabledClass();
        }
        presenter.saveAndRemoveSelection();
        presenter.isGradualShowAnswersActive = true;
        for (const item in presenter.activeGradualShowAnswersItems) {
            presenter.showCorrectAnswer(item);
        }
    };

    presenter.gradualHideAnswers = function () {
        presenter.isGradualShowAnswersActive = false;
        presenter.turnOnEventListeners();
        presenter.restoreSelection();
        removeDisabledClass();
    };

    function addDisabledClass() {
        presenter.$view.find('.text_selection').addClass(CSS_CLASSES.DISABLED);
    }

    function removeDisabledClass() {
        presenter.$view.find('.text_selection').removeClass(CSS_CLASSES.DISABLED);
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Text_To_Speech" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Configuration" isDefault="true" name="configuration" nameLabel="Text_To_Speech_configuration" type="list">
            <property name="ID" nameLabel="Text_To_Speech_property_id" type="string"/>
            <property displayName="Area" name="Area" nameLabel="Text_To_Speech_area" type="{Main, Header, Footer}"/>
            <property isLocalized="true" name="Title" nameLabel="Text_To_Speech_property_title" type="string"/>
        </property>

        <property isLocalized="true" name="EnterText" nameLabel="Text_To_Speech_property_enter_text" type="string"/>
        <property isLocalized="true" name="ExitText" nameLabel="Text_To_Speech_property_exit_text" type="string"/>
        <property isLocalized="true" name="NewPage" nameLabel="Text_To_Speech_property_new_page" type="string"/>
        <property isLocalized="true" name="PageLangTag" nameLabel="Text_To_Speech_property_page_lang_tag" type="string"/>
        <property name="disableNewPageMessage" nameLabel="Text_To_Speech_property_disable_new_page_message" type="boolean"/>
        <property displayName="Verbose new page message" name="verboseNewPageMessage" nameLabel="Text_To_Speech_property_verbose_new_page_message" type="boolean"/>
    </model>
<css>.addon_Text_To_Speech {
    position: relative;
    width: 100%;
    height: 100%;
}
</css><view/><preview/><presenter>function AddonText_To_Speech_create() {

    function getErrorObject (ec) { return {isValid: false, errorCode: ec}; }

    function getCorrectObject (v) { return {isValid: true, value: v}; }

    function getTextVoiceObject (text, lang) { return {text: text, lang: lang}; }

    function getConfObject (id, area, title, description) {
        return {
            id: id,
            area: area,
            title: title,
            description: description
        };
    }

    var presenter = function () {};

    presenter.savedSentences = [];
    presenter.savedSentencesIndex = -1;
    presenter.saveNextSentences = false;

    presenter.messagesQueue = [];

    presenter.ERROR_CODES = {
        C01: 'Configuration cannot be empty',

        S01: 'Sorry, your browser does not support speech synthesis.'
    };

    presenter.LANGUAGES_CODES = {
        English: 'en-US',
        Polski: 'pl-PL',
        Deutsch: 'de-DE',
        DEFAULT: 'English'
    };

    presenter.AREAS = {
        Main: 'main',
        Header: 'header',
        Footer: 'footer',
        DEFAULT: 'Main'
    };

    // Maximum length of a single utterance. If exceeded, the utterence will be split into multiple shorter ones
    presenter.maxUtteranceLength = 200;

    function parseConfiguration(configuration) {
        if (!configuration) {
            return getErrorObject('C01');
        }

        var addOnsTextToSpeechData = [];
        for (var i = 0; i &lt; configuration.length; i++) {
            var conf = configuration[i];
            addOnsTextToSpeechData.push(getConfObject(
                conf.ID,
                ModelValidationUtils.validateOption(presenter.AREAS, conf.Area),
                conf.Title
            ));
        }

        return getCorrectObject(addOnsTextToSpeechData);
    }

    function parseLanguage(language) {
        return getCorrectObject(presenter.LANGUAGES_CODES[language || 'English']);
    }

    presenter.validateModel = function (model) {
        var validatedConfiguration = parseConfiguration(model['configuration']);
        if (!validatedConfiguration.isValid) {
            return getErrorObject(validatedConfiguration.errorCode);
        }

        return {
            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isValid: true,

            addOnsConfiguration: validatedConfiguration.value,
            enterText: model['EnterText'],
            exitText: model['ExitText'],
            newPage: model['NewPage'] ? model['NewPage'] : "New page",
            pageLangTag: model['PageLangTag'],
            disableNewPageMessage: ModelValidationUtils.validateBoolean(model['disableNewPageMessage']),
            verboseNewPageMessage: ModelValidationUtils.validateBoolean(model['verboseNewPageMessage'])
        }
    };

    function loadVoices () {
        presenter.configuration.voices = window.speechSynthesis.getVoices();
    }

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeDisableNewPageMessage(model);
        upgradedModel = presenter.upgradeVerboseNewPageMessage(model);
        return upgradedModel;
    }

    presenter.upgradeDisableNewPageMessage = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['disableNewPageMessage'] === undefined) {
            upgradedModel['disableNewPageMessage'] = "";
        }

        return upgradedModel;
    }

    presenter.upgradeVerboseNewPageMessage = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (upgradedModel['verboseNewPageMessage'] === undefined) {
            upgradedModel['verboseNewPageMessage'] = "";
        }

        return upgradedModel;
    }

    presenter.presenterLogic = function (view, model, isPreview) {
        presenter.$view = $(view);
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        if (!isPreview) {
            MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.$view.get(0));
            MutationObserverService.setObserver();
        }

        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        loadVoices();
        window.speechSynthesis.onvoiceschanged = function (e) {
            loadVoices();
        };

        if ('speechSynthesis' in window) {

        } else {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, 'S01');
            return false;
        }

        presenter.setVisibility(presenter.configuration.isVisible);

        return false;
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.getAddOnConfiguration = function (area, id) {
        id = Array.isArray(id) ? id[0] : id;
        area = Array.isArray(area) ? area[0] : area;

        for (var i = 0; i &lt; presenter.configuration.addOnsConfiguration.length; i++) {
            var conf = presenter.configuration.addOnsConfiguration[i];

            if (conf.id === id &amp;&amp; conf.area.toLowerCase() === area.toLowerCase()) {
                return conf;
            }
        }

        return {title: '', description: ''};
    };

    function getResponsiveVoiceLanguage (langTag) {
        /* Function at first take language from Responsive Voice text area, next is seeking in Responsive Voice
        * library script and finally return default language */
        const languages = presenter.playerController.getResponsiveVoiceLang();
        const langDict = JSON.parse(languages ? languages : "{}");

        if (!langTag) {
            langTag = document.documentElement.lang;
        }

        if (langDict &amp;&amp; langDict[langTag]) {
            return langDict[langTag];
        } else if (isLanguageInLibrary(langTag)) {
            return getLanguageFromLibrary(langTag);
        }
        return 'UK English Male';
    }

    function getLanguageFromLibrary (langTag) {
        return window.responsiveVoice.responsivevoices.find(responsiveVoice =&gt; responsiveVoice.lang.includes(langTag)).name;
    }

    function isLanguageInLibrary (langTag) {
        return window.responsiveVoice.responsivevoices.some(responsiveVoice =&gt; responsiveVoice.lang.includes(langTag));
    }

    function getSpeechSynthesisLanguage (langTag) {
        if (!langTag) {
            // get lang from document &lt;html lang=""&gt;
            langTag = document.documentElement.lang;
        }

        loadVoices();

        var languages = {
            'en': "en-US",
            'pl': 'pl-PL',
            'de': 'de-DE'
        };
        langTag = languages[langTag] || langTag;

        for (var i=0; i&lt;presenter.configuration.voices.length; i++) {
            if (presenter.configuration.voices[i].lang === langTag) {
                return presenter.configuration.voices[i];
            }
        }

        return presenter.configuration.voices[0];
    }

    function filterTexts (texts, languageGetter) {
        return texts.map(function (t) {
            return {
                lang: languageGetter(t.lang),
                text: t.text ? t.text : ''
            };
        }).filter(function (t) { return t.text !== '' });
    }

    // https://responsivevoice.org/
    function responsiveVoiceSpeak (texts, finalCallback) {
        var textsObjects = filterTexts(texts, getResponsiveVoiceLanguage);
        if (finalCallback === undefined) finalCallback = null;

        var onEndStack = { onend: finalCallback };
        for (var i=textsObjects.length-1; i&gt;=0; i--) {
            var textObject = textsObjects[i];

            if (i === 0) {
                window.responsiveVoice.speak(textObject.text, textObject.lang, onEndStack);
            } else {
                onEndStack.onend = (function (textObject, onEndStack) {
                    return function () {
                        window.responsiveVoice.speak(textObject.text, textObject.lang, onEndStack);
                    }
                })($.extend({}, textObject), $.extend({}, onEndStack));
            }
        }
    }

    presenter.intervalId = null;
    presenter.intervalResume = null;

    // https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis
    function speechSynthesisSpeak (texts, finalCallback) {
        window.speechSynthesis.cancel();

        if (presenter.intervalId != null) {
        clearInterval(presenter.intervalId);
            presenter.intervalId = undefined;
        }
        var onStartExecuted = false;
        // Fix for running speak method after cancelling SpeechSynthesis queue
        if(presenter.intervalResume != null) {
            clearInterval(presenter.intervalResume);
            presenter.intervalResume = undefined;
        }

        // Necessary for Chrome browser because instead stop reading
        if (isChrome()) {
                presenter.intervalResume = setInterval(function () {
                window.speechSynthesis.pause();
                window.speechSynthesis.resume();
            }, 3000);
        }

        presenter.intervalId = setInterval(function() {

            if (window.speechSynthesis.speaking) {
                window.speechSynthesis.cancel();
                return;
            }
            var textsObjects = filterTexts(texts, getSpeechSynthesisLanguage);
            if (textsObjects.length === 0) {
                clearInterval(presenter.intervalId);
                presenter.intervalId = undefined;

                clearInterval(presenter.intervalResume);
                presenter.intervalResume = undefined;
                return;
            }
            if (presenter.intervalId == null) return;
            var utterances = [];
            for (var i=0; i&lt;textsObjects.length; i++) {
                var textObject = textsObjects[i];
                var msg = new SpeechSynthesisUtterance(textObject.text);
                msg.volume = parseFloat(1); // 0 - 1
                msg.rate = parseFloat(1); // 0 - 10
                msg.pitch = parseFloat(1); // 0 - 2
                msg.voice = textObject.lang;
                var currentIntervalId = presenter.intervalId;
                if (i === 0) {
                    msg.onstart = function (event) {
                        onStartExecuted = true;
                        clearInterval(currentIntervalId);
                        if (currentIntervalId !== presenter.intervalId) {
                            window.speechSynthesis.cancel();
                        }
                    };
                }
                if (i === textsObjects.length - 1) {
                    msg.onend = function (event) {
                        if(currentIntervalId === presenter.intervalId){
                            window.speechSynthesis.cancel();
                        }
                        if (finalCallback &amp;&amp; onStartExecuted){
                            onStartExecuted = false;
                            finalCallback();
                            utterances = [];
                        };
                    };
                }

                //this list and "push" is solving the problem on
                //'end' event of SpeechSynthesisUtterance object is not dispatched sometimes
                //https://www.e-learn.cn/content/wangluowenzhang/603510
                utterances.push(msg);
                window.speechSynthesis.speak(msg);
            }
        }, 250);
    }

    function getAltTextOptions(expression) {
        var options = {};
        expression = expression.replace(/.*}\[/g, '');
        expression = expression.replace('\]\[', '|');
        expression = expression.replace('\]', '');
        var optionExp = expression.split('\|');
        for(var i=0;i&lt;optionExp.length;i++) {
            var optionValues = optionExp[i].split(' ');
            if(optionValues.length===2){
                options[optionValues[0]]=optionValues[1];
            }
        }
        return options;
    }

     presenter.parseAltTexts = function(texts){
        for (var i=0; i &lt; texts.length; i++) {
            if (texts[i].text !== null &amp;&amp; texts[i].text !== undefined &amp;&amp; texts[i].text.trim().length &gt; 0)
            {
                // altText elements with a langTag need to be isolated into seperate items
                // in the texts array, so that they can use a different language tag.
                texts[i].text = presenter.cleanBracesInAltText(texts[i].text);
                var match = texts[i].text.match(/\\alt{([^{}|]*?)\|([^{}|]*?)}(\[([a-zA-Z0-9_\- ]*?)\])+/g);
                if (match &amp;&amp; match.length&gt;0) {
                    // get the first altText element with a lang tag.
                    // if there are more, they will not be parsed in this iteration
                    // instead, they will become a part of the tail and will be parsed in future iterations
                    var matchText = match[0].trim();
                    var originalMatchText = matchText;
                    var splitTexts = texts[i].text.split(matchText);
                    var startIndex = texts[i].text.indexOf(matchText);
                    var readableText = matchText.replace(/.*\\alt{[^{}|]*?\|([^{}|]*?)}.*/g,"$1");
                    var options = getAltTextOptions(originalMatchText);
                    var langTag = "";
                    if(options.hasOwnProperty('lang')){
                        langTag = options.lang;
                    }

                    if (langTag.length!==0) {
                        var altTextVoice = getTextVoiceObject(readableText, langTag);

                        if (splitTexts) {
                            if (splitTexts.length &gt; 2) {
                                // It is possible that there will be multiple identical altText elements
                                // if that is the case, all elements of the splitTexts array should be merged
                                // with the exception of the head
                                var newSplitTexts = splitTexts.splice(0, 1);
                                newSplitTexts.push(splitTexts.join(originalMatchText));
                                splitTexts = newSplitTexts;
                            }
                            if (splitTexts.length === 2) {
                                texts[i].text = splitTexts[0];
                                texts.splice(i + 1, 0, getTextVoiceObject(splitTexts[1], texts[i].lang));
                                texts.splice(i + 1, 0, altTextVoice);
                            } else if (splitTexts.length === 1) {
                                texts[i].text = splitTexts[0];
                                if (startIndex === 0) {
                                    texts.splice(i, 0, altTextVoice);
                                } else {
                                    texts.splice(i + 1, 0, altTextVoice);
                                }
                            } else if(splitTexts.length === 0) {
                                texts[i] = altTextVoice;
                            }
                        }
                    } else {
                        //if there is no lang option, there is not reason to create a new element in texts array
                        texts[i].text = texts[i].text.replace(originalMatchText, readableText);
                    }
                }

                // handle altText elements without a langTag
                texts[i].text = texts[i].text.replace(/\\alt{.*?\|(.*?)}/g, '$1');
            }
        }

        // splitting matched texts might create elements with an empty text field. This removes them
        texts = texts.filter(function(element){return element &amp;&amp; element.text &amp;&amp; element.text.trim().length&gt;0});
        return texts;
    };

    presenter.cleanBracesInAltText = function (text) {
        let output = text;
        let remainingText = text;
        let nextAltIndex = remainingText.indexOf('\\alt{');
        let count = 0; //backup in case of issues
        while (nextAltIndex !== -1 &amp;&amp; count &lt; 99) {
            remainingText = remainingText.substring(nextAltIndex + 5);
            let openBrackets = 1;
            for (let i = 0; i &lt; remainingText.length; i++) {
                if (remainingText[i] === "{") openBrackets += 1;
                if (remainingText[i] === "}") openBrackets -= 1;
                if (openBrackets === 0) {
                    let altContent = remainingText.substring(0, i);
                    remainingText = remainingText.substring(i);
                    let cleanAltContent = altContent.replaceAll("{", "").replaceAll("}", "");
                    output = output.replace(altContent, cleanAltContent);
                    break;
                }
            }
            count += 1;
            nextAltIndex = remainingText.indexOf('\\alt{');
        }
        return output;
    }

    // The speak method is overloaded:
    // texts argument can be either an array of TextVoiceObjects, or a String
    // langTag argument is optional and only used when texts is a String
    presenter.speak = function (texts, langTag) {
        var class_ = Object.prototype.toString.call(texts);
        if (class_.indexOf('String') !== -1) {
            texts = [getTextVoiceObject(texts, langTag)];
        }

        presenter.speakWithCallback(texts, null);
    };

    function splitByPunctuationSigns(text) {
        let splitIndexes = findPunctuationSignsIndexes(text);
        return splitByIndexesInConsiderationOfProtectedSyntax(text, splitIndexes);
    }

    function findPunctuationSignsIndexes(text) {
        const regexp = /[.,:;!?\/\\()]/g;
        return Array.from(text.matchAll(regexp)).map(x =&gt; x.index);
    }

    function splitByEndOfSentenceSigns(text) {
        let splitIndexes = findEndOfSentenceSignsIndexes(text);
        return splitByIndexesInConsiderationOfProtectedSyntax(text, splitIndexes);
    }

    function findEndOfSentenceSignsIndexes(text) {
        const regexp = /[.?!;]/g;
        return Array.from(text.matchAll(regexp)).map(x =&gt; x.index);
    }

    function protectTimeSyntaxInText(text, splitIndexes) {
        protectAMAndPMSyntaxInText(text, splitIndexes);
        protectDigitHourSyntaxInText(text, splitIndexes);
    }

    function protectAMAndPMSyntaxInText(text, splitIndexes) {
        const regexp = /(at[\s]+[\d]{1,2}[\s]+)([ap].[m].)/g;
        let matches = text.matchAll(regexp);
        filterSplittingIndexes(matches, splitIndexes);
    }

    function protectDigitHourSyntaxInText(text, splitIndexes) {
        const regexp = /([\s]+)([\d]{1,2}:[\d]{2})/g;
        let matches = text.matchAll(regexp);
        filterSplittingIndexes(matches, splitIndexes);
    }

    function protectNumberSyntaxInText(text, splitIndexes) {
        const regexp = /()([0-9]*[.!?;][0-9])/g;
        let matches = text.matchAll(regexp);
        filterSplittingIndexes(matches, splitIndexes);
    }

    /**
    * Leaves the indexes that are not between the range of the matches' second groups
    * @param matches - RegExp matches, where second groups are texts protected from splitting
    * @param splitIndexes - List of indexes (breaking points) to split text
    * @return undefined
    **/
    function filterSplittingIndexes(matches, splitIndexes) {
        for (const match of matches) {
            const startIndex = match.index + match[1].length;
            const endIndex = startIndex + match[2].length - 1;
            for (let i = 0; i &lt; splitIndexes.length; i++) {
                if (startIndex &lt;= splitIndexes[i] &amp;&amp; splitIndexes[i] &lt;= endIndex) {
                    splitIndexes.splice(i, 1);
                    i--;
                }
            }
        }
    }

    /**
    * Split text considering protected syntax using given indexes.
    * @param text - Text to split
    * @param splitIndexes - List of indexes (breaking points) to split text. A breakpoint will have no effect when referring to a protected syntax.
    * @return split text
    **/
    function splitByIndexesInConsiderationOfProtectedSyntax(text, splitIndexes) {
        protectTimeSyntaxInText(text, splitIndexes);
        protectNumberSyntaxInText(text, splitIndexes);
        return splitByIndexes(text, splitIndexes);
    }

    /**
    * Split text using given indexes.
    * @param text - Text to split
    * @param splitIndexes - List of indexes (breaking points) to split text
    * @return split text
    **/
    function splitByIndexes(text, splitIndexes) {
        splitIndexes.unshift(-1);
        splitIndexes.push(text.length);
        let splitText = [];
        for (let i = 1; i &lt; splitIndexes.length; i++) {
            let textFragment = text.slice(splitIndexes[i-1] + 1, splitIndexes[i]);
            if (textFragment.trim().length &gt; 0) {
                splitText.push(textFragment);
            }
        }
        return splitText;
    }

    presenter.removeUnnecessaryPunctuationMarks = function (texts) {
        texts.forEach(text =&gt; {
            const splittedWords = text.text
                .replaceAll(" \/", ",")
                .split(" ");

            const newSentence = [];
            splittedWords.forEach(word =&gt; {
                if (word.trim().length) {
                    const matchAfterLetter = word.match(/(\w\W)[.,?!]+/); // regex captures at least 2 special characters after word
                    const matchAloneOrSeparated = word.match(/(^|\s)[.,?!(){}\\\/\-\[\]]+/); // regex captures at least 1 special separated character or character at the beginning of sentence
                    if (matchAfterLetter) {
                        newSentence.push(word.replace(matchAfterLetter[0], matchAfterLetter[1]));
                    } else if (matchAloneOrSeparated &amp;&amp; !presenter.isMatchContainSentence(matchAloneOrSeparated)) {
                        return;
                    } else {
                        newSentence.push(word);
                    }
                }
            });
            text.text = newSentence.join(" ");
        });

        return texts.filter(text =&gt; text.text.length);
    };

    presenter.isMatchContainSentence = function (word) {
        if (!word) {
            return;
        }

        return word['input'].match(/[a-zA-Z0-9]+/);
    }

    // Too long utterences may take much too long to load or exceed Speech Synthesis API character limit
    presenter.splitLongTexts = function (texts) {
        let finalSplitTexts = [];
        texts.forEach(text =&gt; {
            if (text === null || text === undefined) {
                return;
            }

            const textLen = text.text.trim().length;
            if (textLen === 0) {
                return;
            }

            if (textLen &lt; presenter.maxUtteranceLength) {
                finalSplitTexts.push(text);
            } else {
                let textFragments = splitLongText(text.text, text.lang);
                finalSplitTexts = finalSplitTexts.concat(textFragments);
            }
        })
        return finalSplitTexts;
    }
    
    function splitLongText(text, lang) {
        return minSplitHandler(text, lang, splitByEndOfSentenceSigns, splitLongSentence);
    }

    function splitLongSentence (sentence, lang) {
        return minSplitHandler(sentence, lang, splitByPunctuationSigns, splitLongSentenceByMaxUtterances);
    }
    
    function minSplitHandler (text, lang, currentSplitFunc, nextSplitFunc) {
        let finalSplitTexts = [];
        let splitTexts = currentSplitFunc(text);
        splitTexts.forEach((splitText) =&gt; {
            const textLength = splitText.trim().length;
            if (textLength === 0) {
                return;
            }

            if (textLength &lt; presenter.maxUtteranceLength) {
                finalSplitTexts.push({
                    text: splitText,
                    lang: lang
                });
            } else {
                let textFragments = nextSplitFunc(splitText, lang);
                finalSplitTexts = finalSplitTexts.concat(textFragments);
            }
        })
        return finalSplitTexts;
    }

    // creates an array of utterances from a single text that exceeds max utterance length and has no natural break points
    function splitLongSentenceByMaxUtterances (sentence, lang) {
        let finalSplitTexts = [];
        let sentenceLen = sentence.trim().length;
        let maxSplitLen = sentenceLen / (Math.floor(sentenceLen / presenter.maxUtteranceLength) + 1);
        let workString = '';
        let words = sentence.split(/\s/);
        for (let k = 0; k &lt; words.length; k++) {
            workString += words[k] + ' ';
            if (workString.length &gt; maxSplitLen) {
                finalSplitTexts.push({
                    text: workString,
                    lang: lang
                });
                workString = '';
            }
        }
        finalSplitTexts.push({
            text: workString,
            lang: lang
        });
        return finalSplitTexts;
    }

    presenter.speakWithCallback = function (texts, callback) {
        texts = presenter.parseAltTexts(texts);
        presenter.saveSentences(texts);
        if (isChrome()) {
            presenter.amplifyABeforeColon(texts);
        }
        presenter.readText(texts, callback);
    };

    /**
    * Amplify the letter 'A' or 'a' before colon
    * @param texts
    * @return
    **/
    presenter.amplifyABeforeColon = function (texts) {
        const regex = /([Aa][\s]*?):/;
        for (let i = 0; i &lt; texts.length; i++) {
            texts[i].text = texts[i].text.replace(regex, "$1,");
        }
    }

    function isChrome () {
        const isEdge = navigator.userAgent.indexOf('Edge') !== -1 &amp;&amp; (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob);
        const isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1;
        const isSafari = navigator.userAgent.toLowerCase().indexOf('safari/') &gt; -1;
        return (!isOpera &amp;&amp; !isEdge &amp;&amp; !!navigator.webkitGetUserMedia) || isElectron() || isSafari;
    }

    presenter.readText = function(texts, callback) {
        texts = presenter.splitLongTexts(texts);
        texts = presenter.removeUnnecessaryPunctuationMarks(texts);

        if (window.responsiveVoice) {
            responsiveVoiceSpeak(texts, callback);
            return;
        }

        if ('speechSynthesis' in window) {
            speechSynthesisSpeak(texts, callback);
            return;
        }

        if (callback) {
            callback();
        }
    };

    var delayedSpeakInterval = null; // only used by speakWhenIdle, which is why they are here and not at the top of the file
    var delayedSpeakTimeout = null;
    //This method works like speakWithCallback, except that it waits for TTS to be idle instead of interrupting it
    presenter.speakWhenIdle = function (data, callback) {
        if(delayedSpeakTimeout) {
            clearTimeout(delayedSpeakTimeout);
            delayedSpeakTimeout = null;
        }
         if(delayedSpeakInterval) {
            clearInterval(delayedSpeakInterval);
            delayedSpeakInterval = null;
        }

        function setSpeakInterval (data, callback) {
            delayedSpeakInterval = setInterval(function () {
                var speechSynthSpeaking = false;
                var responsiveVoiceSpeaking = false;

                // Detect if TTS is idle
                if ('speechSynthesis' in window) {
                    speechSynthSpeaking = window.speechSynthesis.speaking;
                }
                if (window.responsiveVoice) {
                    responsiveVoiceSpeaking = window.responsiveVoice.isPlaying();
                }

                if (!speechSynthSpeaking &amp;&amp; !responsiveVoiceSpeaking) {
                    // If TTS is idle, pass data to TTS and break the loop
                    clearInterval(delayedSpeakInterval);
                    delayedSpeakInterval = null;
                    presenter.speakWithCallback(data, callback);
                }
            }, 100);
        }

        /*
        * The timeout is used to ensure that if animation is triggered by another addon,
        * that addon has the opportunity to use TTS first, since animation acts as feedback
        */
        delayedSpeakTimeout = setTimeout(function(){ setSpeakInterval(data, callback); }, 200);
    };

    presenter.playTitle = function (area, id, langTag) {
        if (area &amp;&amp; id) {
            var textVoices = [getTextVoiceObject(presenter.getAddOnConfiguration(area, id).title, langTag)];
            var module = null;
            if(0 === area.toLowerCase().localeCompare("main")){
                module = presenter.playerController.getModule(id);
            } else if (0 === area.toLowerCase().localeCompare("footer")) {
                module = presenter.playerController.getFooterModule(id);
            } else if (0 === area.toLowerCase().localeCompare("header")) {
                module = presenter.playerController.getHeaderModule(id);
            }
            if (module !== undefined &amp;&amp; module !== null &amp;&amp; module.hasOwnProperty('getTitlePostfix')) {
                textVoices.push(getTextVoiceObject(module.getTitlePostfix(), langTag));
            }
            presenter.speak(textVoices);
        }
    };

    presenter.playPageTitle = function () {
        var textVoices = [];
        textVoices.push(getTextVoiceObject(presenter.configuration.newPage,''));
        if (!presenter.configuration.disableNewPageMessage) {
            textVoices.push(getTextVoiceObject(presenter.playerController.getPageTitle(),presenter.configuration.pageLangTag));
        }
        if (presenter.configuration.verboseNewPageMessage) {
            var moduleId = presenter.playerController.getKeyboardController().getCurrentWCAGPresenterId();
            var moduleArea = presenter.playerController.getKeyboardController().getCurrentWCAGPresenterArea();
            textVoices.push(getTextVoiceObject(presenter.getAddOnConfiguration(moduleArea, moduleId).title, ''));
        }
        presenter.speak(textVoices);
    };

    presenter.playEnterText = function () {
        var textVoices = [getTextVoiceObject(presenter.configuration.enterText)];
        if (presenter.configuration.verboseNewPageMessage) {
            var moduleId = presenter.playerController.getKeyboardController().getCurrentWCAGPresenterId();
            var moduleArea = presenter.playerController.getKeyboardController().getCurrentWCAGPresenterArea();
            textVoices.push(getTextVoiceObject(presenter.getAddOnConfiguration(moduleArea, moduleId).title, ''));
        }
        presenter.speak(textVoices);
    };

    presenter.playExitText = function () {
        presenter.speak([getTextVoiceObject(presenter.configuration.exitText)]);
    };

    presenter.getModulesOrder = function () {
        return presenter.configuration.addOnsConfiguration.map(function (c) {
            return {
                id: c.id,
                area: c.area
            };
        });
    };

    presenter.saveSentences = function(texts) {
        if (!presenter.saveNextSentences) return;
        presenter.saveNextSentences = false;
        let textVoices = [];
        for (let i = 0; i &lt; texts.length; i++) {
            let speechText = texts[i];
            let splitSpeechTexts = splitLongText(speechText.text, speechText.lang);
            for (let j = 0; j &lt; splitSpeechTexts.length; j++) {
                textVoices.push(window.TTSUtils.getTextVoiceObject(splitSpeechTexts[j].text, speechText.lang));
            }
        }
        if (!presenter.compareSentences(presenter.savedSentences, textVoices)) {
            presenter.savedSentences = textVoices;
            presenter.savedSentencesIndex = -1;
        }
    }

    presenter.clearSavedSentences = function() {
        presenter.savedSentences = [];
        presenter.savedSentencesIndex = -1;
        presenter.saveNextSentences = false;
    }

    presenter.setSaveNextSentences = function(value) {
        presenter.saveNextSentences = value;
    }

    presenter.readNextSavedSentence = function() {
        if (presenter.savedSentences.length == 0) {
            presenter.savedSentencesIndex = -1;
            return;
        }
        presenter.savedSentencesIndex += 1;
        if (presenter.savedSentencesIndex &gt;= presenter.savedSentences.length) presenter.savedSentencesIndex = presenter.savedSentences.length - 1;
        presenter.readCurrentSavedSentence();
    }

    presenter.readPrevSavedSentence = function() {
        if (presenter.savedSentences.length == 0) {
            presenter.savedSentencesIndex = -1;
            return;
        }
        presenter.savedSentencesIndex -= 1;
        if (presenter.savedSentencesIndex &lt; 0) presenter.savedSentencesIndex = 0;
        presenter.readCurrentSavedSentence();
    }

    presenter.readCurrentSavedSentence = function() {
        presenter.readText([presenter.savedSentences[presenter.savedSentencesIndex]], null);
    }

    presenter.compareSentences = function(sentences1, sentences2) {
        if (sentences1.length != sentences2.length) return false;
        for (var i = 0; i &lt; sentences1.length; i++) {
            if (sentences1[i].lang != sentences2[i].lang) return false;
            if (sentences1[i].text != sentences2[i].text) return false;
        }
        return true;
    }

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.executeCommand = function (name, params) {
        if (!presenter.configuration.isValid) {
            return;
        }

        var commands = {
            "show": presenter.show,
            "hide": presenter.hide,
            "speak": function(params) {
                    if (params.length === 2) {
                        presenter.speak(params[0], params[1]);
                    } else if (params.length === 1) {
                        presenter.speak(params[0]);
                    }
                },
            "playTitle": function(params) {
                    if (params.length === 3) {
                        presenter.playTitle(params[0],params[1],params[2]);
                    } else if (params.length === 2) {
                        presenter.playTitle(params[0],params[1]);
                    }
                },
            "playEnterText": presenter.playEnterText,
            "playExitText": presenter.playExitText,
            "getModulesOrder": presenter.getModulesOrder
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.getState = function () {
        return JSON.stringify({
            addOnsConfiguration: presenter.configuration.addOnsConfiguration,
            enterText: presenter.configuration.enterText,
            exitText: presenter.configuration.exitText,

            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
    };

    presenter.setState = function (state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var parsedState = JSON.parse(state);

        presenter.configuration.addOnsConfiguration = parsedState.addOnsConfiguration;
        presenter.configuration.enterText = parsedState.enterText;
        presenter.configuration.exitText = parsedState.exitText;

        presenter.configuration.isVisible = parsedState.isVisible;
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.cancelSpeechSynthesis = function(){
        if (window.responsiveVoice) {
            window.responsiveVoice.cancel();
        }
        if ('speechSynthesis' in window) {
            window.speechSynthesis.cancel();
        }
        if(presenter.speechSynthInterval!==null){
            clearInterval(presenter.intervalId);
            presenter.intervalId = undefined;

            clearInterval(presenter.intervalResume);
            presenter.intervalResume = undefined;
        }
    };

    presenter.destroy = function () {
        presenter.cancelSpeechSynthesis();
        presenter.configuration = null;
        presenter.$view = null;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Timer" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Mode" nameLabel="Timer_property_mode" type="{Timer, Stopwatch}"/>
        <property name="Time" nameLabel="Timer_property_time" type="string"/>
        <property name="Immediate start" nameLabel="Timer_property_immediate_start" type="boolean"/>
        <property name="Send event every second" nameLabel="Timer_property_send_event_every_second" type="boolean"/>
        <property name="Show hours" nameLabel="Timer_property_show_hours" type="boolean"/>
        <property name="Enable reset" nameLabel="Timer_property_enable_reset" type="boolean"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Timer_property_speech_texts" type="staticlist">
            <property name="TimerStarted" nameLabel="Timer_property_timer_started" type="staticrow">
                    <property name="TimerStarted" nameLabel="Timer_property_timer_started" type="string"/>
            </property>
            <property name="TimerStopped" nameLabel="Timer_property_timer_stopped" type="staticrow">
                    <property name="TimerStopped" nameLabel="Timer_property_timer_stopped" type="string"/>
            </property>
            <property name="TimerEnded" nameLabel="Timer_property_timer_ended" type="staticrow">
                    <property name="TimerEnded" nameLabel="Timer_property_timer_ended" type="string"/>
            </property>
            <property name="StopwatchStarted" nameLabel="Timer_property_stopwatch_started" type="staticrow">
                    <property name="StopwatchStarted" nameLabel="Timer_property_stopwatch_started" type="string"/>
            </property>
            <property name="StopwatchStopped" nameLabel="Timer_property_stopwatch_stopped" type="staticrow">
                    <property name="StopwatchStopped" nameLabel="Timer_property_stopwatch_stopped" type="string"/>
            </property>
            <property name="Hours" nameLabel="Timer_property_hours" type="staticrow">
                    <property name="Hours" nameLabel="Timer_property_hours" type="string"/>
            </property>
            <property name="Minutes" nameLabel="Timer_property_minutes" type="staticrow">
                    <property name="Minutes" nameLabel="Timer_property_minutes" type="string"/>
            </property>
            <property name="Seconds" nameLabel="Timer_property_seconds" type="staticrow">
                    <property name="Seconds" nameLabel="Timer_property_seconds" type="string"/>
            </property>
        </property>
    </model>
<css>.timer-wrapper .separator:after{
    content: ':';
}
</css><view>&lt;div class="timer-wrapper"&gt;
    &lt;span class="hours"&gt;&lt;/span&gt;
    &lt;span class="separator hours-separator"&gt;&lt;/span&gt;
    &lt;span class="minutes"&gt;&lt;/span&gt;
    &lt;span class="separator minutes-separator"&gt;&lt;/span&gt;
    &lt;span class="seconds"&gt;&lt;/span&gt;
&lt;/div&gt;
</view><preview>&lt;div class="timer-wrapper"&gt;
    &lt;span class="hours"&gt;&lt;/span&gt;
    &lt;span class="separator hours-separator"&gt;&lt;/span&gt;
    &lt;span class="minutes"&gt;&lt;/span&gt;
    &lt;span class="separator minutes-separator"&gt;&lt;/span&gt;
    &lt;span class="seconds"&gt;&lt;/span&gt;
&lt;/div&gt;
</preview><presenter>function AddonTimer_create(){

	var presenter = function(){};
	presenter.keyboardControllerObject = null;
	presenter.isWCAGOn = false;

	presenter.configuration = {
		isValid: true,
		mode: "",
		isFired: false,
		time: 0,
		alarmTime: 0,
		immediateStart: false,
		sendEvent: false,
		isVisible: false,
		showHours: false,
		enableReset: false,
		errorCode: "Z01",
		addonID: ""
	};

	presenter.state = {
		isFired: false,
		isVisible: false,
		currentTime: 0,
		interval: null,
		nowStart: false,
		showHours: false
	};

	presenter.ERROR_CODES = {
		"Z01": "None",
        "T01": "Wrong time"
    };

	presenter.MODE = {
        'Timer': 'Timer',
		'Stopwatch': 'Stopwatch',
        DEFAULT: 'Timer'
    };

	presenter.DEFAULT_TTS_PHRASES = {
        TIMER_STARTED: "Timer started",
        TIMER_STOPPED: "Timer stopped",
        TIMER_ENDED: "Timer ended",
        STOPWATCH_STARTED: "Stopwatch started",
        STOPWATCH_STOPPED: "Stopwatch stopped",
        HOURS: "Hours",
        MINUTES: "Minutes",
        SECONDS: "Seconds"
    };

	presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };

	presenter.createEventData = function(value) {
        return {
            source : presenter.configuration.addonID,
            value : value
        };
    };

	presenter.triggerEvent = function(value) {
        var eventData = presenter.createEventData(value);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
		if (value === 'end') {
			presenter.readOnEnd();
		}
    };

	presenter.executeCommand = function (name, params) {

		var commands = {
			'start': presenter.start,
			'stop': presenter.stop,
			'show': presenter.show,
			'hide': presenter.hide,
			'reset': presenter.reset,
			'getTime': presenter.getTime,
			'addTime': presenter.addTime,
			'setTime': presenter.setTime,
			'showHoursPart': presenter.showHoursPart,
			'hideHoursPart': presenter.hideHoursPart
		};

		Commands.dispatch(commands, name, params, presenter);
	};

	presenter.start = function(shouldReadTTS = true) {
        presenter.countDown();
		presenter.state.nowStart = true;
		if (shouldReadTTS) {
			presenter.readOnStart();
		}
    };

	presenter.stop = function(shouldReadTTS = true) {
        clearInterval(presenter.state.interval);
		presenter.state.isFired = false;
		presenter.state.nowStart = false;
		if (shouldReadTTS) {
			presenter.readOnStop();
		}
    };

	presenter.show = function() {
		presenter.displayhhmmss(presenter.state.currentTime);
        presenter.setVisibility(true);
		if (presenter.state.showHours) {
			presenter.showHoursPart();
		}
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
		presenter.$view.find('.timer-wrapper .hours').css("visibility", "hidden");
		presenter.$view.find('.timer-wrapper .hours-separator').css("visibility", "hidden");
    };

	presenter.updateVisibility = function() {
        (presenter.state.isVisible) ? presenter.show() : presenter.hide();
    };

	presenter.reset = function() {
		if (presenter.configuration.enableReset) {
			clearInterval(presenter.state.interval);
			presenter.state.isFired = false;
			presenter.state.currentTime = presenter.configuration.time;
			presenter.displayhhmmss(presenter.state.currentTime);
			presenter.state.isVisible = presenter.configuration.isVisible;
			presenter.setVisibility(presenter.state.isVisible);
			presenter.state.nowStart = presenter.configuration.immediateStart;
			if (presenter.state.nowStart) {
				presenter.start();
			}
		}
    };

	presenter.getTime = function() {
		var newTime = timeToHMMSS(presenter.state.currentTime);
		var time = (newTime.hours &gt; 0|| presenter.state.showHours) ? (newTime.hours +':') : ('');
		time += newTime.minutes+':'+newTime.seconds;
		return time;
    };

	presenter.setTime = function(time) {
		var timeInSeconds = validateTime('Timer',time,false);
		if (timeInSeconds.isValid) {
			presenter.state.currentTime = timeInSeconds.time;
			presenter.displayhhmmss(presenter.state.currentTime);
		}
    };

	presenter.addTime = function(time) {
		if (!isNaN(time)) {
			presenter.state.currentTime += parseInt(time);
			presenter.displayhhmmss(presenter.state.currentTime);
		}
    };

	presenter.showHoursPart = function() {
        presenter.state.showHours = true;
		presenter.$view.find('.timer-wrapper .hours').css("visibility", "visible");
		presenter.$view.find('.timer-wrapper .hours-separator').css("visibility", "visible");
    };

	presenter.hideHoursPart = function() {
        presenter.state.showHours = false;
		presenter.$view.find('.timer-wrapper .hours').css("visibility", "hidden");
		presenter.$view.find('.timer-wrapper .hours-separator').css("visibility", "hidden");
    };

    presenter.setVisibility = function(isVisible) {
        presenter.state.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

	var validateTime = function (mode,time,isAlarm) {
		if ((mode === 'Timer' &amp;&amp; isAlarm === true) || (mode === 'Stopwatch' &amp;&amp; isAlarm === false) || (mode === 'Stopwatch' &amp;&amp; isAlarm === true &amp;&amp; time === '')) {
			return {
				time: 0,
				isValid: true
			}
		}
		var parts = time.split(':');
		var timeInSeconds;

		if (parts.length !== 2 &amp;&amp; parts.length !== 3) {
			return {
				isValid: false,
				errorCode: "T01"
			}
		} else {
			$.each(parts, function(k,v){
				if(v !== parseInt(v) || v &lt; 0 || v &gt; 59) {
					return {
						isValid: false,
						errorCode: "T01"
					}
				}
			});
		}
		if (parts.length === 2) {
			timeInSeconds = parseInt(parts[0])*60 + parseInt(parts[1]);
		} else {
			timeInSeconds = parseInt(parts[0])*3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]);
		}
		return {
			time: timeInSeconds,
			isValid: true
		};
	};

	presenter.displayhhmmss = function (totalSeconds) {
		var newTime = timeToHMMSS(totalSeconds);
		presenter.$view.find('.timer-wrapper .hours').html(newTime.hours);
		presenter.$view.find('.timer-wrapper .minutes').html(newTime.minutes);
		presenter.$view.find('.timer-wrapper .seconds').html(newTime.seconds);
	};

	var timeToHMMSS = function (totalSeconds) {
		var hour = Math.floor(totalSeconds / 3600);
		var minute = Math.floor((totalSeconds - (hour * 3600)) / 60);
		var second = totalSeconds - (hour * 3600) - (minute * 60);
		if (minute &lt; 10) {
			minute = '0' + minute;
		}
		if (second &lt; 10) {
			second = '0' + second;
		}
		return {
			hours: hour,
			minutes: minute,
			seconds: second
		}
	};

	presenter.countDown = function () {
		if (!presenter.state.isFired) {
			presenter.state.isFired = true;
			presenter.state.interval = setInterval(function() {
				if (presenter.configuration.mode === 'Timer' &amp;&amp; presenter.state.currentTime &gt; 0) {
					presenter.state.currentTime--
				} else if (presenter.configuration.mode === 'Stopwatch') {
					presenter.state.currentTime++;
				}
				var newTime = timeToHMMSS(presenter.state.currentTime);
				if (presenter.state.isVisible) {
					presenter.displayhhmmss(presenter.state.currentTime);
				}
				if (presenter.configuration.sendEvent) {
					var eventTime = (newTime.hours &gt; 0) ? (newTime.hours +':') : ('');
					eventTime += newTime.minutes+':'+newTime.seconds;
					presenter.triggerEvent(eventTime);
				}
				if (presenter.configuration.mode === 'Timer' &amp;&amp; presenter.state.currentTime === 0) {
					presenter.triggerEvent('end');
					presenter.state.isFired = false;
					presenter.state.nowStart = false;
					clearInterval(presenter.state.interval);
				}
				if (presenter.configuration.mode === 'Stopwatch' &amp;&amp; presenter.configuration.alarmTime === presenter.state.currentTime) {
					presenter.triggerEvent('time');
				}
			}, 1000);
		}
	};

	presenter.upgradeModel = function (model) {
		return presenter.upgradeModelWithSpeechTexts(model);
	};

	presenter.upgradeModelWithSpeechTexts = function (model) {
		const upgradedModel = {};
		const defaultValue = {
			TimerStarted: {TimerStarted: ''},
			TimerStopped: {TimerStopped: ''},
			TimerEnded: {TimerEnded: ''},
			StopwatchStarted: {StopwatchStarted: ''},
			StopwatchStopped: {StopwatchStopped: ''},
			Hours: {Hours: ''},
			Minutes: {Minutes: ''},
			Seconds: {Seconds: ''}
		};
        $.extend(true, upgradedModel, model);

		if (!upgradedModel.hasOwnProperty('speechTexts')) {
			upgradedModel['speechTexts'] = defaultValue;
		}

		return upgradedModel;
	};

	presenter.validateModel = function(model) {
		var mode = ModelValidationUtils.validateOption(presenter.MODE, model.Mode);
		var validatedTime = validateTime(mode,model['Time'],false);
		if (!validatedTime.isValid) {
			return {
				isValid: false,
				errorCode: validatedTime.errorCode
			};
		}
		var validatedAlarm = validateTime(mode,model['Time'],true);
		if (!validatedAlarm.isValid) {
			return {
				isValid: false,
				errorCode: validatedAlarm.errorCode
			};
		}
		return {
			isValid: true,
			mode: mode,
			isFired: false,
			time: validatedTime.time,
			alarmTime: validatedAlarm.time,
			immediateStart: ModelValidationUtils.validateBoolean(model['Immediate start']),
			sendEvent: ModelValidationUtils.validateBoolean(model['Send event every second']),
			isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
			showHours: ModelValidationUtils.validateBoolean(model["Show hours"]),
			enableReset: ModelValidationUtils.validateBoolean(model["Enable reset"]),
			addonID: model['ID']
		};
	};

	presenter.initiate = function(view, model, isPreview) {
		const upgradedModel = presenter.upgradeModel(model);
		const validatedModel = presenter.validateModel(model);
		if (!validatedModel.isValid) {
			DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.errorCode);
			return;
		}
		presenter.configuration = validatedModel;
		presenter.view = view;
		presenter.$view = $(view);
		presenter.state.currentTime = presenter.configuration.time;
		presenter.state.nowStart = presenter.configuration.immediateStart;
		presenter.state.showHours = presenter.configuration.showHours;
		if (!presenter.state.showHours) {
			presenter.$view.find('.timer-wrapper .hours').css("visibility", "hidden");
			presenter.$view.find('.timer-wrapper .hours-separator').css("visibility", "hidden");
		}
        presenter.state.isVisible = presenter.configuration.isVisible;
		presenter.displayhhmmss(presenter.state.currentTime);
		if (!isPreview &amp;&amp; presenter.state.nowStart) {
			presenter.countDown();
		}
		MutationObserverService.createDestroyObserver(presenter.configuration.addonID, presenter.destroy, presenter.view);
		MutationObserverService.setObserver();
		presenter.setSpeechTexts(upgradedModel);
		presenter.buildKeyboardController();
	};

	presenter.destroy = function () {
        clearInterval(presenter.state.interval);
    };

	presenter.run = function(view, model){
		presenter.initiate(view, model,false);
	};

	presenter.createPreview = function(view, model){
		presenter.initiate(view, model,true);
	};

	presenter.getErrorCount = function(){
		return 0;
	};

	presenter.getMaxScore = function(){
		return 0;
	};

	presenter.getScore = function(){
		return 0;
	};

	presenter.getState = function(){
		return JSON.stringify({
            currentTime : presenter.state.currentTime,
            visible : presenter.state.isVisible,
			nowStart : presenter.state.nowStart,
			showHours : presenter.state.showHours
        });
	};

	presenter.setState = function(state){
		var parsedState = JSON.parse(state);
		presenter.state.isVisible = parsedState.visible;
		presenter.state.showHours = parsedState.showHours;
		presenter.state.currentTime = parsedState.currentTime;
		presenter.displayhhmmss(presenter.state.currentTime);
		presenter.state.nowStart = parsedState.nowStart;
		if (presenter.state.nowStart) {
			presenter.start(false);
		} else {
			presenter.stop(false);
		}
		presenter.updateVisibility();
		if (presenter.state.isVisible &amp;&amp; presenter.state.showHours) {
			presenter.showHoursPart();
		}
	};

	presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

	presenter.keyboardController = function (keycode, isShiftKeyDown, event) {
		presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
	};
	
	presenter.buildKeyboardController = function () {
		const elements = presenter.getElementsForKeyboardNavigation();
		presenter.keyboardControllerObject =
			new TimerKeyboardController(elements);
	};

	presenter.setSpeechTexts = function (model) {
		const speechTexts = model['speechTexts'];
		presenter.speechTexts = {
			TimerStarted: presenter.DEFAULT_TTS_PHRASES.TIMER_STARTED,
			TimerStopped: presenter.DEFAULT_TTS_PHRASES.TIMER_STOPPED,
			TimerEnded: presenter.DEFAULT_TTS_PHRASES.TIMER_ENDED,
			StopwatchStarted: presenter.DEFAULT_TTS_PHRASES.STOPWATCH_STARTED,
			StopwatchStopped: presenter.DEFAULT_TTS_PHRASES.STOPWATCH_STOPPED,
			Hours: presenter.DEFAULT_TTS_PHRASES.HOURS,
			Minutes: presenter.DEFAULT_TTS_PHRASES.MINUTES,
			Seconds: presenter.DEFAULT_TTS_PHRASES.SECONDS,
		};

		if(!speechTexts || $.isEmptyObject(speechTexts)) {
			return;
		}

		presenter.speechTexts = {
			TimerStarted: TTSUtils.getSpeechTextProperty(
				speechTexts.TimerStarted.TimerStarted,
				presenter.speechTexts.TimerStarted
			),
			TimerStopped: TTSUtils.getSpeechTextProperty(
				speechTexts.TimerStopped.TimerStopped,
				presenter.speechTexts.TimerStopped
			),
			TimerEnded: TTSUtils.getSpeechTextProperty(
				speechTexts.TimerEnded.TimerEnded,
				presenter.speechTexts.TimerEnded
			),
			StopwatchStarted: TTSUtils.getSpeechTextProperty(
				speechTexts.StopwatchStarted.StopwatchStarted,
				presenter.speechTexts.StopwatchStarted
			),
			StopwatchStopped: TTSUtils.getSpeechTextProperty(
				speechTexts.StopwatchStopped.StopwatchStopped,
				presenter.speechTexts.StopwatchStopped
			),
			Hours: TTSUtils.getSpeechTextProperty(
				speechTexts.Hours.Hours,
				presenter.speechTexts.Hours
			),
			Minutes: TTSUtils.getSpeechTextProperty(
				speechTexts.Minutes.Minutes,
				presenter.speechTexts.Minutes
			),
			Seconds: TTSUtils.getSpeechTextProperty(
				speechTexts.Seconds.Seconds,
				presenter.speechTexts.Seconds
			),
		};
	};

	presenter.getElementsForKeyboardNavigation = function () {
		return presenter.$view.find('.timer-wrapper');
	};

	function TimerKeyboardController(elements) {
		KeyboardController.call(this, elements, 1);
	}

	TimerKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
	TimerKeyboardController.prototype.constructor = TimerKeyboardController;

	TimerKeyboardController.prototype.getCurrentElement = function () {
		return this.getTarget(this.keyboardNavigationCurrentElement, false);
	};

	TimerKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

	TimerKeyboardController.prototype.select = function (event) {
		if (event){
			event.preventDefault();
		}

		this.selectAction();
	};

	TimerKeyboardController.prototype.enter = function (event) {
		KeyboardController.prototype.enter.call(this, event);
		this.readCurrentElement();
	};

	TimerKeyboardController.prototype.readCurrentElement = function () {
		presenter.speak(presenter.getTextToRead());
	};

	presenter.readOnStart = function () {
		const textToRead = presenter.configuration.mode === 'Timer' ?
			presenter.speechTexts.TimerStarted : presenter.speechTexts.StopwatchStarted;

		presenter.speak(textToRead, true);
	};


	presenter.readOnEnd = function () {
		presenter.speak(presenter.speechTexts.TimerEnded);
	};

	presenter.readOnStop = function () {
		const textToRead = presenter.configuration.mode === 'Timer' ?
			presenter.speechTexts.TimerStopped : presenter.speechTexts.StopwatchStopped;

		presenter.speak(textToRead, true);
	};
	
	presenter.speak = function (data, shouldRunWithOffset = false) {
		const tts = presenter.getTextToSpeechOrNull();
		const isWCAGActive = presenter.playerController.isWCAGOn();
		if (!(tts &amp;&amp; isWCAGActive)) { return; }

		if (shouldRunWithOffset) {
			setTimeout(() =&gt; {
				presenter.waitForEndSpeaking(tts.speak, data);
			}, 400);
		} else {
			tts.speak(data);
		}
	};

	presenter.waitForEndSpeaking = function(callback, data, timeout = 5000) {
        const startTime = Date.now();

        let interval = setInterval(() =&gt; {
            if (!window.speechSynthesis.speaking) {
                clearInterval(interval);
                callback(data);
            } else if (Date.now() - startTime &gt; timeout) {
                clearInterval(interval);
            }
        }, 100);
    };

	presenter.getTextToSpeechOrNull = function () {
		if (presenter.playerController) {
			return presenter.playerController.getModule('Text_To_Speech1');
		}

		return null;
	};

	presenter.getTextToRead = function () {
		if (presenter.state.currentTime === 0) {
			return `0 ${presenter.speechTexts.Seconds}`;
		}

		const timerValues = presenter.getTime().split(':');
		let hours, minutes, seconds = 0;
		let text = '';
		if (timerValues.length &gt; 2) {
			hours = +timerValues[0];
			minutes = +timerValues[1];
			seconds = +timerValues[2];
		} else {
			minutes = +timerValues[0];
			seconds = +timerValues[1];
		}

		if (hours &amp;&amp; +hours &gt; 0) {
			text += `${hours} ${presenter.speechTexts.Hours},`;
		}

		if (minutes &amp;&amp; +minutes &gt; 0 || !text) {
			text += `${minutes} ${presenter.speechTexts.Minutes},`;
		}

		if (seconds &amp;&amp; +seconds &gt; 0 || !text) {
			text += `${seconds} ${presenter.speechTexts.Seconds}`;
		}

		return text;
	};

	presenter.isSelectable = function(isWCAGOn) {
	    return isWCAGOn;
	}

	return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="TrueFalse" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Is not an activity" name="isNotActivity" nameLabel="TrueFalse_property_is_not_activity" type="boolean"/>
		<property name="Multi" nameLabel="TrueFalse_property_multi" type="boolean"/>
		<property isDefault="true" name="Questions" nameLabel="TrueFalse_property_questions" type="list">
			<property isLocalized="true" name="Question" nameLabel="TrueFalse_property_question" type="html"/>
			<property name="Answer" nameLabel="TrueFalse_property_answer" type="string"/>
		</property>
		<property name="Choices" nameLabel="TrueFalse_property_choices" type="list">
			<property isLocalized="true" name="Choice" nameLabel="TrueFalse_property_choice" type="html"/>
		</property>
		<property name="Lang attribute" nameLabel="TrueFalse_property_lang_attribute" type="string"/>
		<property name="Speech texts" nameLabel="TrueFalse_property_speech_texts" type="staticlist">
			<property displayName="selected" name="Selected" type="staticrow">
                <property name="selected" nameLabel="TrueFalse_property_selected" type="string"/>
            </property>
			<property displayName="deselected" name="Deselected" type="staticrow">
                <property name="deselected" nameLabel="TrueFalse_property_deselected" type="string"/>
            </property>
			<property displayName="correct" name="Correct" type="staticrow">
                <property name="correct" nameLabel="TrueFalse_property_correct" type="string"/>
            </property>
			<property displayName="incorrect" name="Incorrect" type="staticrow">
                <property name="incorrect" nameLabel="TrueFalse_property_incorrect" type="string"/>
            </property>
		</property>
		<property displayName="Is disabled" name="isDisabled" nameLabel="TrueFalse_property_is_disabled" type="boolean"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
		<property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
	</model>
<css>.ic_page{
    direction:ltr;
}

#container * {
    direction:inherit;
}

.tf_radio,
.tf_checkbox {
    margin:0 0 1em 0;
    width: 100%;
    height: 100%;
    text-align:left;
    position: absolute;
    left: 0;
    top: 0;
    border-spacing:0 5px;
}

.tf_radio_row,
.tf_checkbox_row {
}

.tf_radio_image,
.tf_checkbox_image {
    cursor:pointer;
    border-collapse: separate;
    width:70px;
    min-width:70px;
    height:26px;
    min-height:26px;
    border-bottom:1px solid #111;
    border-top:1px solid #111;

}

.tf_radio_image.last,
.tf_checkbox_image.last {
    border-radius: 0 10px 10px 0;
    -moz-border-radius: 0 10px 10px 0;
    border-right:1px solid #111;
}

.tf_radio_text,
.tf_checkbox_text {
    margin:5px 0 0 4px;
    width:70px;
    min-width:70px;
    height:26px;
    min-height:26px;
    text-align:center;
}

.tf_radio_question.first,
.tf_checkbox_question.first {
    border:0;
}

.tf_radio_question,
.tf_checkbox_question {
    margin-top:5px;
    border-bottom:1px solid #111;
    border-top:1px solid #111;
    border-left:1px solid #111;
    padding:10px;
    border-radius: 10px 0 0 10px;
    -moz-border-radius: 10px 0 0 10px;
}

.tf_radio_image.up div {
    background-image: url('resources/tf-radio-image-up.png');
}

.tf_radio_image.up.disabled div {
    background-image: url('resources/tf-radio-image-up-disabled.png');
}

.tf_radio_image.down div {
    background-image: url('resources/tf-radio-image-down.png');
}

.tf_radio_image.down.correct div,
.tf_radio_image.down.correct-answer div{
    background-image: url('resources/tf-radio-image-down-correct.png');
}

.tf_radio_image.down.wrong div {
    background-image: url('resources/tf-radio-image-down-wrong.png');
}

.tf_checkbox_image.up div {
    background-image: url('resources/tf-checkbox-image-up.png');
}

.tf_checkbox_image.up.disabled div {
    background-image: url('resources/tf-checkbox-image-up-disabled.png');
}

.tf_checkbox_image.down div {
    background-image: url('resources/tf-checkbox-image-down.png');
}

.tf_checkbox_image.down.correct div,
.tf_checkbox_image.down.correct-answer div{
    background-image: url('resources/tf-checkbox-image-down-correct.png');
}

.tf_checkbox_image.down.wrong div {
    background-image: url('resources/tf-checkbox-image-down-wrong.png');
}

.tf_checkbox_image div,
.tf_radio_image div {
    width: 100%;
    height: 100%;
    background: no-repeat left center;
}

.printable_addon_TrueFalse .radio-container,
.printable_addon_TrueFalse .checkbox-container {
    position: relative;
}

.printable_addon_TrueFalse .radio-container input,
.printable_addon_TrueFalse .checkbox-container input {
    position: absolute;
    visibility: hidden;
}

.printable_addon_TrueFalse .radio-container span {
    display: block;
    width: 0.8em;
    height: 0.8em;
    border: solid 1px;
    border-radius: 50%;
}

.printable_addon_TrueFalse .checkbox-container span {
    display: block;
    width: 0.8em;
    height: 0.8em;
    border: solid 1px;
    border-radius: 2px;
}

.printable_addon_TrueFalse .radio-container span:after,
.printable_addon_TrueFalse .checkbox-container span:after {
    display: none;
    content: "";
}

.printable_addon_TrueFalse .radio-container input:checked ~ span:after,
.printable_addon_TrueFalse .checkbox-container input:checked ~ span:after {
    display: block;
}

.printable_addon_TrueFalse .radio-container span:before {
    position: relative;
    top: 18.5%;
    left: 18.5%;
    width: 12%;
    height: 12%;
    border: solid black;
    border-radius: 50%;
}

.printable_addon_TrueFalse .checkbox-container span:before {
    position: relative;
    width: 0.5em;
    height: 0.5em;
    border: solid black;
}

.correctAnswerMark::after {
    content: "\2714";
}

.incorrectAnswerMark::after {
    content: "\2716";
}
</css><view/><preview/><presenter>function AddonTrueFalse_create() {

    function getTextVoiceObject (text, lang) { return {text: text, lang: lang}; }

    var presenter = function () {};

    presenter.type = "";
    presenter.lastEvent = null;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.keyboardNavigationActive = false;
    presenter.keyboardNavigationCurrentElement = null;
    presenter.keyboardNavigationElements = [];
    presenter.keyboardNavigationElementsLen = 0;
    presenter.printableState = null;
    presenter.didUserRespond = false;

    var possibleChoices = [];
    var multi = false;
    var isNotActivity = false;
    var questions = [];
    var playerController;
    var printableController;
    var eventBus; // Modules communication
    var tts;
    var selectedSpeechText = "selected";
    var deselectedSpeechText = "deselected";
    var correctSpeechText = "correct";
    var incorrectSpeechText = "incorrect";

    var QUESTION_AND_CHOICES_REQUIRED = "At least 1 question and 2 choices are required.";
    var INDEX_OUT_OF_RANGE = "Index is out of range.";
    var isWCAGOn = false;

    presenter.textParser = null;

    presenter.isSelectionCorrect = function (question, selection) {
        var answers = question.Answer.split(',');

        for (var i = 0; i &lt; answers.length; i++) {
            if (parseInt(answers[i], 10) === selection) {
                return true;
            }
        }

        return false;
    };

    var score = function () {
        var score = { 'score': 0, 'maxScore': 0, 'errorCount': 0 };
        for (var i = 0; i &lt; questions.length + 1; i++) {
            var j = 0;
            var row = presenter.$view.find('#' + i);
            if (i &gt; 0) {
                var values = (questions[i - 1].Answer).split(',');
                score.maxScore += values.length;
                row.children().each(function () {
                    if (isCorrectAnswer($(this), values, j)) {
                        score.score += 1;
                    } else if (isWrongAnswer($(this), values, j)) {
                        score.errorCount += 1;
                    }
                    j++;
                });
            }
        }
        return score;
    };

    var workMode = function (reset) {
        presenter.$view.find(".tf_" + presenter.type + "_image").each(function () {
            var image = $(this);
            image.removeClass("disabled wrong correct correct-answer");
            if (reset) {
                image.removeClass("down").addClass("up");
            }
        });

        presenter.$view.find('.tf_radio_question').each(function() {
            $(this).removeClass('disabled');
        });
    };

    var markElements = function () {
        for (var i = 0; i &lt; questions.length + 1; i++) {
            var j = 0;
            var row = presenter.$view.find('#' + i);
            if (i &gt; 0) {
                var values = (questions[i - 1].Answer).split(',');
                row.children().each(function () {
                    if ($(this).hasClass("disabled") &amp;&amp; j &gt; 0) return;
                    $(this).addClass("disabled");
                    if (isCorrectAnswer($(this), values, j)) {
                        $(this).addClass("correct");
                    } else if (isWrongAnswer($(this), values, j)) {
                        $(this).addClass("wrong");
                    }
                    j++;
                });
            }
        }
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        presenter.textParser = new TextParserProxy(getTextParser());
    };

    function whichQuestion(row, table) {
        var questionNumber = 0;

        $(table).find('tr').each(function (index) {
            if ($(this)[0] == $(row)[0]) {
                questionNumber = index;

                return false;
            }
        });

        return questionNumber;
    }

    function whichAnswer(element, row) {
        var answerNumber = 0;
        $(row).find('.tf_' + presenter.type + '_image').each(function (index) {
            if ($(this)[0] == $(element)[0]) {
                answerNumber = index + 1; // Answers are counted from 1 to n

                return false;
            }

            return true;
        });

        return answerNumber;
    }

    presenter.createEventData = function (item, wasSelected, isSelectionCorrect) {
        return {
            'source': presenter.addonID,
            'item': item,
            'value': wasSelected ? '0' : '1',
            'score': isSelectionCorrect ? '1' : '0'
        };
    };

    presenter.createAllOKEventData = function () {
        return {
            'source': presenter.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };
    };

    presenter.createRowOKEventData = function (row) {
        return {
            'source': presenter.addonID,
            'item': row + '-all',
            'value': '',
            'score': ''
        }
    };

    function clickLogic(element) {
        var sendEvent = true;
        var wasSelected = false;
        if (presenter.isDisabled) return;

        if (!$(element).hasClass("disabled")) {
            if (multi) {
                if ($(element).hasClass("down")) {
                    wasSelected = true;
                    $(element).removeClass("down").addClass("up");
                } else {
                    $(element).removeClass("up").addClass("down");
                }
            } else {
                sendEvent = !$(element).hasClass("down");

                $(element).parent().children(".tf_" + presenter.type + "_image").each(function () {
                    $(this).removeClass("down").addClass("up");
                });

                $(element).removeClass("up").addClass("down");
            }

            if (sendEvent) {
                var selectedQuestion = whichQuestion($(element).parent(), $(element).parent().parent());
                var selectedAnswer = whichAnswer($(element), $(element).parent());
                var itemStr = selectedQuestion.toString() + '-' + selectedAnswer.toString();
                var isSelectionCorrect = presenter.isSelectionCorrect(questions[selectedQuestion - 1], parseInt(selectedAnswer, 10));

                var eventData = presenter.createEventData(itemStr, wasSelected, isSelectionCorrect);
                eventBus.sendEvent('ValueChanged', eventData);

                if (multi &amp;&amp; presenter.isRowOK(selectedQuestion)) {
                    var rowOKEventData = presenter.createRowOKEventData(selectedQuestion);
                    eventBus.sendEvent('ValueChanged', rowOKEventData);

                }

                if (presenter.isAllOK()) {
                    var allOKEventData = presenter.createAllOKEventData();
                    eventBus.sendEvent('ValueChanged', allOKEventData);
                }
            }
        }
    }

    function handleClickActions(view) {
        if(presenter.isDisabled) return;


        var $elements = $(view).find(".tf_" + presenter.type + "_image");
        
        if (!MobileUtils.isMobileUserAgent(window.navigator.userAgent)){
            $elements.hover(function(){
                $(this).addClass('mouse-hover');
                }, function(){
                $(this).removeClass('mouse-hover');
            });
        }
        
        $elements.on('touchstart', function (e) {
            e.stopPropagation();
            e.preventDefault();

            presenter.lastEvent = e;
        });

        $elements.on('touchend', function (e) {
            e.stopPropagation();
            e.preventDefault();

            if (presenter.lastEvent.type != e.type) {
                clickLogic($(e.target).parent());
            }
        });

        $elements.click(function (e) {
            e.stopPropagation();
            clickLogic(this);
        });
    }

    presenter.addTabindex = function (element, value) {
        element.attr("tabindex", value);
    };

    function generatePossibleChoicesRow(row) {
        row.append('&lt;td class="tf_' + presenter.type + '_question first" role="gridcell"&gt;&amp;nbsp;&lt;/td&gt;');

        for (var k = 0; k &lt; possibleChoices.length; k++) {
            var td = $('&lt;td class="tf_' + presenter.type + '_text" role="gridcell"&gt;' + possibleChoices[k].Choice + '&lt;/td&gt;');

            if (presenter.isTabindexEnabled) {
                presenter.addTabindex(td, 0);
            }

            row.append(td);
        }
    }

    function generateQuestionElement(row, rowID) {
        const question = questions[rowID - 1].Question;
        const parsedQuestion = presenter.textParser.parse(question);
        const td = $('&lt;td class="tf_' + presenter.type + '_question" role="gridcell"&gt;' + parsedQuestion + '&lt;/td&gt;');

        if (presenter.isTabindexEnabled) {
            presenter.addTabindex(td, 0);
        }

        row.append(td);
    }

    function generateRowContent(row, rowID) {
        generateQuestionElement(row, rowID);

        for (var i = 0; i &lt; possibleChoices.length; i++) {
            if (i === (possibleChoices.length - 1)) {
                row.append('&lt;td class="tf_' + presenter.type + '_image up last"&gt;&lt;/td&gt;');
            } else {
                row.append('&lt;td class="tf_' + presenter.type + '_image up"&gt;&lt;/td&gt;');
            }
            var innerElement = document.createElement('div');
            $(innerElement).css('color','rgba(0,0,0,0.0)');
            $(innerElement).css('font-size','0px');
            var text = $("&lt;div&gt;" + possibleChoices[i].Choice + "&lt;/div&gt;").text();
            var altText = document.createTextNode(text);
            $(innerElement).append(altText);
            innerElement.setAttribute('role', 'gridcell');

            if (presenter.isTabindexEnabled) {
                presenter.addTabindex($(innerElement), 0);
            }

            $(row).find('td:last-child').append(innerElement);
        }
    }

    function rowIndexed () {
        var count = possibleChoices.length + 1;
        return questions.reduce(function (acc, q, index) {
            acc.push({
                start: (index * count),
                end: ((index + 1) * count) - 1
            });
            return acc;
        }, []);
    }

    presenter.generateTableContent = function AddonTrueFalse_generateTableContent(table, view) {
        table.setAttribute('role', 'table');
        for (var rowID = 0; rowID &lt; questions.length + 1; rowID++) {
            $(table).append('&lt;tr class="tf_' + presenter.type + '_row" id=' + rowID + ' role="row"&gt;&lt;/tr&gt;');
            var row = $(view).find('#' + rowID);

            if (rowID === 0) {
                generatePossibleChoicesRow(row);
            } else {
                var answers = (questions[rowID - 1].Answer).split(',');
                for (var m = 0; m &lt; answers.length; m++) {
                    var answer = parseInt(answers[m]);
                    if (answer &gt; possibleChoices.length || answer &lt;= 0) {
                        $(view).html(INDEX_OUT_OF_RANGE);

                        break;
                    }
                }

                generateRowContent(row, rowID);
            }
        }
    };

    function getSpeechTexts(model) {
        var speechTexts = model['Speech texts'];

        if (speechTexts['Selected']['selected'] !== '' &amp;&amp; speechTexts['Selected']['selected'] !== undefined) {
            selectedSpeechText = speechTexts['Selected']['selected'];
        }

        if (speechTexts['Deselected']['deselected'] !== '' &amp;&amp; speechTexts['Deselected']['deselected'] !== undefined) {
            deselectedSpeechText = speechTexts['Deselected']['deselected'];
        }

        if (speechTexts['Correct']['correct'] !== '' &amp;&amp; speechTexts['Correct']['correct'] !== undefined) {
            correctSpeechText = speechTexts['Correct']['correct'];
        }

        if (speechTexts['Incorrect']['incorrect'] !== '' &amp;&amp; speechTexts['Incorrect']['incorrect'] !== undefined) {
            incorrectSpeechText = speechTexts['Incorrect']['incorrect'];
        }
    }

    var makeView = function (view, model, isPreview) {
        possibleChoices = model['Choices'];
        questions = model['Questions'];
        presenter.langAttribute = model['Lang attribute'];
        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.isDisabled = false; // At start addon is always enabled, so we need to reset flag and set correct value.
        presenter.isDisabledByDefault = ModelValidationUtils.validateBoolean(model["isDisabled"]);
        presenter.$view.attr('lang', presenter.langAttribute);

        getSpeechTexts(model);

        if (notAllRequiredParameters(questions, possibleChoices)) {
            return $(view).html(QUESTION_AND_CHOICES_REQUIRED);
        }

        multi = model['Multi'] === 'True';

        if (model['isNotActivity'] != undefined){
            isNotActivity = (model['isNotActivity'] === 'True');
        }
        else {
            isNotActivity = false;
        }

        presenter.type = multi ? "checkbox" : "radio";
        var table = document.createElement('table');

        $(table).addClass('tf_' + presenter.type);
        $(table).attr("cellspacing", 0).attr("cellpadding", 0);
        $(view).append(table);

        presenter.generateTableContent(table, view);

        if (!isPreview) {
            handleClickActions(view);
            presenter.setVisibility(presenter.isVisible);

            if (presenter.isDisabledByDefault) {
                presenter.disable();
            } else {
                presenter.enable();
            }
        }

        if (presenter.textParser !== null &amp;&amp; !isPreview) { // Actions performed only in Player mode
            presenter.textParser.connectLinks($(view));
        }

        if (!isPreview) {
            presenter.$view.find('.tf_' + presenter.type + '_image' + ',.tf_' + presenter.type + '_question:not(.first)').each(function(index, element){
                presenter.keyboardNavigationElements[index] = $(element);
            });

            presenter.keyboardNavigationElementsLen = presenter.keyboardNavigationElements.length;
        }
    };

    presenter.setPlayerController = function (controller) {
        playerController = controller;
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.upgradeModel = function (model) {
        if (model['Speech texts'] === undefined) {
            model['Speech texts'] = {
                'Selected': {
                    'selected': ''
                },

                'Deselected': {
                    'deselected': ''
                },

                'Correct': {
                    'correct': ''
                },

                'Incorrect': {
                    'incorrect': ''
                }
            }
        }

        return model;
    };

    presenter.validateModel = function(model) {
        presenter.isTabindexEnabled = ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']);
    };

    presenter.run = function (view, model) {
        model = presenter.upgradeModel(model);
        presenter.$view = $(view);
        presenter.textParser = new TextParserProxy(playerController.getTextParser());

        presenter.validateModel(model);

        presenter.addonID = model.ID;
        makeView(view, model, false);

        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];

        if (!eventBus) { return; }

        for (var i = 0; i &lt; events.length; i++) {
            eventBus.addEventListener(events[i], this);
        }
    };

    function isCorrectAnswer(element, values, index) {
        return values.indexOf(index.toString()) &gt;= 0 &amp;&amp; element.hasClass("down");
    }

    function isWrongAnswer(element, values, index) {
        return values.indexOf(index.toString()) &lt; 0 &amp;&amp; element.hasClass("down");
    }

    function notAllRequiredParameters(questions, possibleChoices) {
        return !questions[0].Question || possibleChoices.length &lt; 2 || !possibleChoices[0].Choice || !possibleChoices[1].Choice;
    }

    function getSelectedElements() {
        var selectedElements = [];
        var i = 0;
        presenter.$view.find(".tf_" + presenter.type + "_image").each(function () {
            selectedElements[i] = $(this).hasClass("down");
            i++;
        });
        return selectedElements;
    }

    presenter.createPreview = function (view, model) {
        model = presenter.upgradeModel(model);
        presenter.$view = $(view);
        makeView(view, model, true);
    };

    presenter.getState = function () {
        function getStateBase(selectedElements) {
            return {
            selectedElements: selectedElements,
            isVisible: presenter.isVisible,
            isDisabled: presenter.isDisabled
            }
        }

        var state = {};
        if (presenter.isShowAnswers()) {
            state = getStateBase(presenter.currentState); // This is saved on ShowAnswers
        } else {
            state = getStateBase(getSelectedElements());
        }

        return JSON.stringify(state);
    };

    presenter.setState = function (state) {
        if (!state) {
            return;
        }

        var i = 0;
        var selectedElements;
        var parsedState = JSON.parse(state);
        if(typeof parsedState.isVisible !== "undefined"){
            selectedElements = parsedState.selectedElements;
            presenter.setVisibility(parsedState.isVisible);
            presenter.isVisible = parsedState.isVisible;
        }else{
            selectedElements = parsedState;
        }

        presenter.$view.find(".tf_" + presenter.type + "_image").each(function () {
            if (selectedElements[i] == true) {
                $(this).addClass("down");
                $(this).removeClass("up");
            }
            i++;
        });

        //  For backward compatibility in old lessons
        if (parsedState.isDisabled === undefined) {
            parsedState.isDisabled = false;
        }

        if(!presenter.isShowAnswersActive) {
            if (parsedState.isDisabled) {
                presenter.disable();
            } else {
                presenter.enable();
            }
        }
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;
        presenter.didUserRespond = false;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.didUserRespond = true;
        presenter.printableState = JSON.parse(state);
    };

    presenter.setShowErrorsMode = function () {
        if (isNotActivity) {
            return;
        }

        presenter.isErrorMode = true;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        markElements();
    };

    presenter.setWorkMode = function () {
        if (isNotActivity) {
            return;
        }

        presenter.isErrorMode = false;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        workMode(false);
    };

    presenter.reset = function () {
        presenter.isErrorMode = false;
        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;

        if (presenter.currentState) {
            delete presenter.currentState;
        }

        workMode(true);
        presenter.setVisibility(presenter.isVisibleByDefault);
        presenter.isVisible = presenter.isVisibleByDefault;

        if (presenter.isDisabledByDefault) {
            presenter.disable();
        } else {
            presenter.enable();
        }
    };

    presenter.getErrorCount = function () {
        if (isNotActivity) return 0;

        if (presenter.isShowAnswers()) {
            return presenter.currentScore.errorCount;
        }

        return score().errorCount;
    };

    presenter.getMaxScore = function () {
        if (isNotActivity) return 0;

        if (presenter.isShowAnswers()) {
            return presenter.currentScore.maxScore;
        }

        return score().maxScore;
    };

    presenter.getScore = function () {
        if (isNotActivity) return 0;

        if (presenter.isShowAnswers()) {
            return presenter.currentScore.score;
        }

        return score().score;
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.isErrorMode) {
            return;
        }

        var commands = {
            'isAllOK': presenter.isAllOK,
            'isSelected': presenter.isSelectedCommand,
            'markAsCorrect': presenter.markAsCorrectCommand,
            'markAsWrong': presenter.markAsWrongCommand,
            'markAsEmpty': presenter.markAsEmptyCommand,
            'removeMark': presenter.removeMarkCommand,
            'isAttempted' : presenter.isAttemptedCommand,
            'show': presenter.show,
            'hide': presenter.hide,
            'reset' : presenter.reset,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'enable' : presenter.enable,
            'disable' : presenter.disable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.isRowOK = function (selectedQuestion) {
        var correctAnswersLength = 0;
        var rowAnswers = questions[selectedQuestion - 1].Answer.split(',');
        var row = presenter.$view.find('#' + selectedQuestion);

        for (var i = 0; i &lt; row.children('.down').length; i++) {
            var selectedAnswer = $(row.children('.down')[i]).index();
            var isSelectionCorrect = presenter.isSelectionCorrect(questions[selectedQuestion - 1], parseInt(selectedAnswer, 10));
            if (isSelectionCorrect) {
                correctAnswersLength++;
            }
        }

        return rowAnswers.length === correctAnswersLength;
    };

    presenter.isSelected = function (rowIndex, answerIndex) {
        if (answerIndex &lt; 1) return false;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var row = presenter.$view.find('#' + rowIndex);
        var el = row.children()[answerIndex];
        return $(el).hasClass("down");
    };


    presenter.markAsCorrect = function (rowIndex, answerIndex) {
        if(presenter.isDisabled) return;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var row = presenter.$view.find('#' + rowIndex);
        if (rowIndex &gt; 0) {
            var el = row.children()[answerIndex];
            $(el).addClass("disabled");
            $(el).addClass("correct");
         }
    };

    presenter.markAsWrong = function (rowIndex, answerIndex) {
        if(presenter.isDisabled) return;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var row = presenter.$view.find('#' + rowIndex);
        if (rowIndex &gt; 0) {
            var el = row.children()[answerIndex];
            $(el).addClass("disabled");
            $(el).addClass("wrong");
        }
    };

    presenter.markAsEmpty = function (rowIndex, answerIndex) {
        if(presenter.isDisabled) return;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var row = presenter.$view.find('#' + rowIndex);
        if (rowIndex &gt; 0) {
            var el = row.children()[answerIndex];
            $(el).addClass("disabled");
        }
    };

    presenter.removeMark = function (rowIndex, answerIndex) {
        if(presenter.isDisabled) return;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var row = presenter.$view.find('#' + rowIndex);
        if (rowIndex &gt; 0) {
            var el = row.children()[answerIndex];
            $(el).removeClass("wrong").removeClass("correct");
        }
    };

    presenter.isAttempted = function () {
        if (isNotActivity) return true;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        var isAttempted = false;
        for (var rowIndex = 0; rowIndex &lt; questions.length + 1; rowIndex++) {
            var answerIndex = 0;
            var row = presenter.$view.find('#' + rowIndex);
            if (rowIndex &gt; 0) {
                row.children().each(function () {
                    if (presenter.isSelected(rowIndex, answerIndex)) {
                        isAttempted = true;
                        return false; // break;
                    }
                    answerIndex++;
                    return true;
                });
            }
        }
        return isAttempted;
    };

    presenter.isSelectedCommand = function (params) {
        presenter.isSelected(parseInt(params[0], 10), parseInt(params[1], 10));
    };

    presenter.isAttemptedCommand = function () {
         return presenter.isAttempted();
    };

    presenter.markAsEmptyCommand = function (params) {
        presenter.markAsEmpty(parseInt(params[0], 10), parseInt(params[1], 10));
    };

    presenter.markAsWrongCommand = function (params) {
        presenter.markAsWrong(parseInt(params[0], 10), parseInt(params[1], 10));
    };

    presenter.removeMarkCommand = function (params) {
        presenter.removeMark(parseInt(params[0], 10), parseInt(params[1], 10));
    };

    presenter.markAsCorrectCommand = function (params) {
        presenter.markAsCorrect(parseInt(params[0], 10), parseInt(params[1], 10));
    };

    presenter.onEventReceived = function (eventName, data) {
        if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName === "HideAnswers") {
            presenter.hideAnswers();
        } else if (eventName === "GradualShowAnswers") {
            if (!presenter.isGradualShowAnswersActive) {
                presenter.currentScore = score();
                presenter.currentState = getSelectedElements();
                presenter.isGradualShowAnswersActive = true;
            }
            if (data.moduleID === presenter.addonID) {
                workMode(true);
                presenter.gradualShowAnswers(parseInt(data.item, 10));
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.gradualHideAnswers();
        }
    };

    presenter.gradualShowAnswers = function (itemIndex) {
        presenter.setCorrectAnswers(itemIndex + 1); // don't ask
    }

    presenter.gradualHideAnswers = function () {
        presenter.isGradualShowAnswersActive = false;
        workMode(true);

        var state = JSON.stringify({
            "selectedElements": presenter.currentState,
            "isVisible": presenter.isVisible
        });

        presenter.setState(state);

        delete presenter.currentState;
    }

    presenter.isShowAnswers = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    }

    presenter.getActivitiesCount = function () {
        return questions.length;
    }

    presenter.disable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.isDisabled = true;
        presenter.$view.addClass("disabled");
    };

    presenter.enable = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.isDisabled = false;
        presenter.$view.removeClass("disabled");
    };

    presenter.showAnswers = function () {
        if (isNotActivity || presenter.isShowAnswersActive) {
            return;
        }

        presenter.currentScore = score();
        presenter.isShowAnswersActive = true;
        presenter.currentState = getSelectedElements();
        presenter.isErrorMode = false;
        workMode(true);

        presenter.setCorrectAnswers(questions.length)
    };

    presenter.setCorrectAnswers = function (length) {
       for (var i = 1; i &lt; length + 1; i++) {
            var $row = presenter.$view.find('#' + i);
            var correctValues = (questions[i - 1].Answer).split(',');

            var trueFalseImages = $row.find(".tf_" + presenter.type + "_image");
            trueFalseImages.addClass('disabled');

            for (var j = 0; j &lt; correctValues.length; j++) {
                // empty string split by separator will return array with empty element
                if (correctValues[j].length &gt; 0) {
                    var index = parseInt(correctValues[j], 10) + 1;
                    var $element = $row.find(".tf_" + presenter.type + "_image:nth-child(" + index + ")");

                    $element.addClass('down correct-answer');
                    $element.removeClass('up');
                }
            }
        }
    }

    presenter.hideAnswers = function () {
        if (isNotActivity || !presenter.isShowAnswersActive) {
            return;
        }

        workMode(true);

        var state = JSON.stringify({
            "selectedElements": presenter.currentState,
            "isVisible": presenter.isVisible
        });

        presenter.setState(state);

        delete presenter.currentState;
        presenter.isShowAnswersActive = false;
    };

    function getTextToSpeech () {
        if (tts) {
            return tts;
        }

        tts = playerController.getModule('Text_To_Speech1');
        return tts;
    }

    function getActivatedElement () {
        return presenter.$view.find('.keyboard_navigation_active_element');
    }

    function getElementIndex(element) {
        var div = element.parent(),
            parent = div.parent(),
            list = parent.find('td');

        return $(list).index(div);
    }

    function getChoice(index) {
        var $topRowElement = presenter.$view.find('#0');
        var $choiceElement = $topRowElement.children().eq(index);
        return window.TTSUtils.getTextVoiceArrayFromElement($choiceElement,presenter.langAttribute);
    }

    function readOption(readSelection) {
        var tts = getTextToSpeech();
        if (tts) {
            var $active = getActivatedElement(),
                elementIndex = getElementIndex($active),
                choiceArray = getChoice(elementIndex);
            
            if ($active.hasClass('tf_' + presenter.type + '_question')) {
                speak(window.TTSUtils.getTextVoiceArrayFromElement($active,presenter.langAttribute));
                return;
            }

            if (!readSelection) {
                if ($active.parent().hasClass('down')) {
                    if (presenter.isErrorMode) {
                        if ($active.parent().hasClass('correct')) {
                            speak(choiceArray.concat([getTextVoiceObject(selectedSpeechText + " " + correctSpeechText, "")]))
                        }
                        if($active.parent().hasClass('wrong')) {
                            speak(choiceArray.concat([getTextVoiceObject(selectedSpeechText + " " + incorrectSpeechText, "")]))
                        }
                    } else {
                        speak(choiceArray.concat([getTextVoiceObject(selectedSpeechText, "")]))
                    }
                } else {
                    speak(choiceArray);
                }
            } else {
                if ($active.parent().hasClass('down')) {
                    speak([getTextVoiceObject(selectedSpeechText, "")]);
                } else {
                    speak([getTextVoiceObject(deselectedSpeechText, "")]);
                }
            }
        }
    }

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = getTextToSpeech();
        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        event.preventDefault();
        presenter.shiftPressed = event.shiftKey;

        var keys = {
            ENTER: 13,
            ESCAPE: 27,
            SPACE: 32,
            ARROW_LEFT: 37,
            ARROW_UP: 38,
            ARROW_RIGHT: 39,
            ARROW_DOWN: 40,
            TAB: 9
        };

        function mark_current_element(new_position_index){
            if (presenter.keyboardNavigationCurrentElement) {
                if(presenter.keyboardNavigationCurrentElement.find('div').length &gt; 0) {
                    presenter.keyboardNavigationCurrentElement.find('div').removeClass('keyboard_navigation_active_element');
                } else {
                    presenter.keyboardNavigationCurrentElement.removeClass('keyboard_navigation_active_element');
                }
            }
            presenter.keyboardNavigationCurrentElementIndex = new_position_index;
            presenter.keyboardNavigationCurrentElement = presenter.keyboardNavigationElements[new_position_index];
            if(presenter.keyboardNavigationCurrentElement.find('div').length &gt; 0) {
                presenter.keyboardNavigationCurrentElement.find('div').addClass('keyboard_navigation_active_element');
            } else {
                presenter.keyboardNavigationCurrentElement.addClass('keyboard_navigation_active_element');
            }
        }

        var enter = function (){
            if (isShiftKeyDown) {
                if (presenter.keyboardNavigationActive){
                    escape();
                    presenter.isKeyboardOpened = false;
                }
                return;
            }

            if (!presenter.keyboardNavigationActive) {
                presenter.keyboardNavigationActive = true;
                mark_current_element(0);
                readOption(false);
            } else {
                readOption(false);
            }
        };

        function swicht_element(move, checkDirection){
            var rows = rowIndexed();

            var currentRow = rows.filter(function (row) {
                return row.start &lt;= presenter.keyboardNavigationCurrentElementIndex &amp;&amp; row.end &gt;= presenter.keyboardNavigationCurrentElementIndex;
            })[0];

            var new_position_index = presenter.keyboardNavigationCurrentElementIndex + move;

            if(checkDirection &amp;&amp; currentRow &amp;&amp; (new_position_index &lt; currentRow.start || new_position_index &gt; currentRow.end)) {
                return;
            }

            if (new_position_index &gt;= presenter.keyboardNavigationElementsLen) {
                new_position_index = new_position_index - move;
            } else if (new_position_index &lt; 0) {
                new_position_index = presenter.keyboardNavigationCurrentElementIndex;
            }
            mark_current_element(new_position_index);
        }

        var next_element = function (){
            swicht_element(1, true);
            readOption(false);
        };

        var previous_element = function (){
            swicht_element(-1, true);
            readOption(false);
        };

        var next_question = function () {
            swicht_element(possibleChoices.length + 1, false);
            readOption(false);
        };

        var previous_question = function () {
            swicht_element(-(possibleChoices.length + 1), false);
            readOption(false);
        };

        var mark = function (){
            if (presenter.isErrorMode) {
                return;
            }
            presenter.keyboardNavigationCurrentElement.click();
            readOption(true);
        };

        var escape = function (){
            if (!presenter.keyboardNavigationActive){
                return;
            }
            presenter.keyboardNavigationActive = false;
            presenter.keyboardNavigationCurrentElement.find('div').removeClass('keyboard_navigation_active_element');
            presenter.keyboardNavigationCurrentElement.removeClass('keyboard_navigation_active_element');
            presenter.keyboardNavigationCurrentElement = null;
        };

        function tabHandler() {
            swicht_element(isShiftKeyDown ? -1 : 1, true);
            readOption(false);
        }

        var mapping = {};
        mapping[keys.ENTER] = enter;
        mapping[keys.ESCAPE] = escape;
        mapping[keys.SPACE] = mark;
        mapping[keys.ARROW_LEFT] = previous_element;
        mapping[keys.ARROW_UP] = previous_question;
        mapping[keys.ARROW_RIGHT] = next_element;
        mapping[keys.ARROW_DOWN] = next_question;
        mapping[keys.TAB] = tabHandler;

        try {
            mapping[keycode]();
        } catch (er) {
        }

    };

    presenter.setPrintableController = function (controller) {
        printableController = controller;
        presenter.textParser = new TextParserProxy(printableController.getTextParser());
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var model = presenter.upgradeModel(model);
        var isMulti = model.Multi === 'True';
        var userAnswers = getUserResponses();
        var choiceLength = model.Choices.length

        var $view = $("&lt;div&gt;&lt;/div&gt;");
        $view.attr('id', model.ID);
        $view.addClass('printable_addon_TrueFalse');
        $view.css("max-width", model["Width"]+"px");
        var $table = $("&lt;table&gt;&lt;/table&gt;");
        var $tbody = $("&lt;tbody&gt;&lt;/tbody&gt;");

        //Header row
        var $trHead = $("&lt;tr&gt;&lt;/tr&gt;");
        $trHead.append("&lt;td&gt;&lt;/td&gt;");
        for (var i = 0; i &lt; choiceLength; i++) {
            var choice = model.Choices[i];
            var $td = $("&lt;td&gt;&lt;/td&gt;");
            $td.html(choice.Choice);
            if (isMulti &amp;&amp; showAnswers &amp;&amp; presenter.didUserRespond) {
                $td.attr('colspan', '2');
            } else if (!isMulti &amp;&amp; showAnswers &amp;&amp; presenter.didUserRespond &amp;&amp; i === (choiceLength - 1)) {
                $td.attr('colspan', '2');
            }
            $trHead.append($td);
        }
        $tbody.append($trHead);

        //Question rows
        for (var i = 0; i &lt; model.Questions.length; i++) {
            var question = model.Questions[i];
            var $tr = $("&lt;tr&gt;&lt;/tr&gt;");

            var $questionCell = $("&lt;td&gt;&lt;/td&gt;");
            $questionCell.html(presenter.textParser.parse(question.Question));
            $tr.append($questionCell);

            var answers = [];
            if (showAnswers) answers = question.Answer.split(',');
            var boxType = isMulti ? "checkbox" : "radio";

            for (var j = 0; j &lt; choiceLength; j++) {
                var $td = $("&lt;td&gt;&lt;/td&gt;");

                $td.addClass(`${boxType}-container`);
                $td.addClass(`${boxType}-${i+1}-${j+1}`);
                var $inputDiv = $("&lt;div&gt;&lt;/div&gt;");
                $inputDiv.addClass("placeholder");
                $td.append($inputDiv);
                var $checkbox = $("&lt;input type=\"checkbox\"&gt;");
                var userAnswerIndex = i * choiceLength + j;
                var $checkboxSpan = $("&lt;span&gt;&lt;/span&gt;");

                if (presenter.didUserRespond &amp;&amp; userAnswers[userAnswerIndex]) {
                    $checkbox.attr("checked", "checked");
                    $checkboxSpan.css('background', 'black');
                } else if (showAnswers &amp;&amp; answers.indexOf((j+1).toString()) != -1 &amp;&amp; !presenter.didUserRespond) {
                    $checkbox.attr("checked", "checked");
                    $checkboxSpan.css('background', 'darkgray');
                }

                $td.append($checkbox);

                $td.append($checkboxSpan);
                $tr.append($td);

                if (showAnswers &amp;&amp; isMulti &amp;&amp; userAnswers[userAnswerIndex]) {
                    var $markCell = $("&lt;td&gt;&lt;/td&gt;");
                    var $markDiv = $("&lt;div&gt;&lt;/div&gt;");
                    isAnswerCorrect(answers, userAnswers, i, j, choiceLength) ? $markDiv.addClass("correctAnswerMark") : $markDiv.addClass("incorrectAnswerMark");
                    $markCell.append($markDiv);
                    $tr.append($markCell);
                } else if (showAnswers &amp;&amp; presenter.didUserRespond &amp;&amp; isMulti) {
                    addCell(answers, userAnswers, $tr, i, choiceLength);
                }
            }
            if (showAnswers &amp;&amp; !isMulti &amp;&amp; didUserRespondOnQuestion(userAnswers, i, choiceLength)) {
                addCell(answers, userAnswers, $tr, i, choiceLength, true);
            }
            $tbody.append($tr);
        }

        $table.append($tbody);
        $view.append($table);
        return $view[0].outerHTML;
    };

    presenter.getScoreWithMetadata = function() {
        var scores = [];
        var allAnswers = [];
        var selectedElements = getSelectedElements();
        for (var i = 0; i &lt; possibleChoices.length; i++) {
            allAnswers.push(possibleChoices[i].Choice.trim());
        }
        for (var i = 0; i &lt; questions.length; i++) {
            var score = {
                userAnswer: "",
                allAnswers: allAnswers,
                isCorrect: false,
            };
            var correctAnswers = questions[i].Answer.split(',');
            firstChoiceIndex = i * possibleChoices.length;
            for (var j = 0; j &lt; possibleChoices.length; j++) {
                if (selectedElements[firstChoiceIndex + j]) {
                    score.userAnswer = allAnswers[j];
                    if (correctAnswers.indexOf((j + 1) + "") != -1) {
                        score.isCorrect = true;
                    }
                }
            }
            scores.push(score);
        }
        return scores;
    }

    function getUserResponses() {
        if (presenter.printableState &amp;&amp; presenter.printableState.hasOwnProperty('selectedElements')) {
            return presenter.printableState['selectedElements']
        }
        return [];
    }

    function addCell(answers, userAnswers, $tableRow, i, choiceLength, shouldAddMark = false) {
        var $td = $("&lt;td&gt;&lt;/td&gt;");
        var $markDiv = $("&lt;div&gt;&lt;/div&gt;");
        if (shouldAddMark) {
            areAnswersCorrect(answers, userAnswers, i, choiceLength) ? $markDiv.addClass("correctAnswerMark")
                : $markDiv.addClass("incorrectAnswerMark");
        }
        $td.append($markDiv);
        $tableRow.append($td);
    }

    function didUserRespondOnQuestion(userAnswers, i, choiceLength) {
        return userAnswers.slice(i * choiceLength, (i + 1) * choiceLength).some(answer =&gt; answer);
    }

    function areAnswersCorrect(correctAnswer, userAnswer, i, choiceLength) {
        var areCorrect = false;
        correctAnswer.forEach(answer =&gt; {
            var index = +answer - 1;
            if (userAnswer[i * choiceLength + index]) {
                areCorrect = true;
            }
        });
        return areCorrect;
    }

    function isAnswerCorrect(correctAnswers, userAnswer, i, j, choiceLength) {
        return correctAnswers.indexOf((j + 1).toString()) !== -1 &amp;&amp; userAnswer[i * choiceLength + j];
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Variable_Storage" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Variables" isDefault="true" name="Variables" nameLabel="Variable_Storage_property_variables" type="list">
            <property displayName="Name" name="Name" nameLabel="Variable_Storage_property_variables_name" type="string"/>
            <property displayName="Start value" name="Start" nameLabel="Variable_Storage_property_variables_start" type="string"/>
        </property>
        <property displayName="Do not reset" name="NotReset" nameLabel="Variable_Storage_property_do_not_reset" type="boolean"/>
    </model>
<css>.variables_storage {
    color: black;
    font-size:12px;
}
</css><view>&lt;div class="variables_addon"&gt;&lt;/div&gt;
</view><preview>&lt;div class="variables_storage"&gt;Variable storage&lt;/div&gt;
</preview><presenter>function AddonVariable_Storage_create(){
    var presenter = function(){}
    presenter.Variables = [];
    presenter.executeCommand = function(name, params) {
        switch(name.toLowerCase()) {
            case 'getVariable'.toLowerCase():
                presenter.getVariable(params[0]);
                break;
            case 'setVariable'.toLowerCase():
                presenter.setVariable(params[0],params[1]);
                break;
            case 'reset'.toLowerCase():
                presenter.reset();
                break;
        }
    }
    function validateVariables(list){
        var variable = [], variableName, variableValue, tmpVariable;
        for (i = 0; i &lt; list.length; i++) {
            variable = [];
            variableName = list[i]['Name'];
            if (variableName != '') {
                variableValue = list[i]['Start'];
                tmpVariable = {
                    name: variableName,
                    startValue: variableValue,
                    currentValue: variableValue
                };
                presenter.Variables.push(tmpVariable);
            }
        };
    }
    presenter.initiate = function(view, model){
        presenter.$view = $(view);
        presenter.model = model;
        presenter.addonID = model.ID;
        validateVariables(presenter.model['Variables']);
        presenter.notReset = false;
        presenter.notReset = ModelValidationUtils.validateBoolean(presenter.model['NotReset']);
    }
    presenter.run = function(view, model){
        presenter.initiate(view, model);
    }
    presenter.createPreview = function(view, model){
        presenter.initiate(view, model);
    }
    presenter.getVariable = function(varName) {
        for (var i = 0; i &lt; presenter.Variables.length; i++) {
            if (presenter.Variables[i]['name'] == varName)
                return presenter.Variables[i]['currentValue'];
        }
        return false;
    }
    presenter.setVariable = function(varName, varValue) {
        var isValue = false;
        for (var i = 0; i &lt; presenter.Variables.length; i++) {
            if (presenter.Variables[i]['name'] == varName) {
                presenter.Variables[i]['currentValue'] = varValue;
                isValue = true;
            }
        }
        if (!isValue) {
            var tmpVariable = {
                name: varName,
                startValue: false,
                currentValue: varValue
            };
            presenter.Variables.push(tmpVariable);
        }
    }
    presenter.reset = function() {
        if (!presenter.notReset) {
            for (var i = 0; i &lt; presenter.Variables.length; i++) {
                presenter.Variables[i]['currentValue'] = presenter.Variables[i]['startValue'];
            }
        }
    }
    presenter.getState = function() {
        var tmpVariablesValue = [], tmpVariablesStart = [], tmpVariablesName = [];
        for (var i = 0; i &lt; presenter.Variables.length; i++) {
            tmpVariablesValue[i] = presenter.Variables[i]['currentValue'];
            tmpVariablesStart[i] = presenter.Variables[i]['startValue'];
            tmpVariablesName[i] = presenter.Variables[i]['name'];
        }
        return JSON.stringify({
            Variables : tmpVariablesValue,
            VariablesName : tmpVariablesName,
            VariableValue : tmpVariablesStart,
            VariablesNumber: presenter.Variables.length
        });
    }
    presenter.setState = function(state) {
        var VariablesNumber = JSON.parse(state).VariablesNumber;
        var tmpVariable;
        for (var i = 0; i &lt; presenter.Variables.length; i++) {
            presenter.Variables[i]['currentValue'] = JSON.parse(state).Variables[i];
        }
        for (; i &lt; VariablesNumber; i++) {
            tmpVariable = {
                name: JSON.parse(state).VariablesName[i],
                startValue: JSON.parse(state).VariableValue[i],
                currentValue: JSON.parse(state).Variables[i]
            };
            presenter.Variables.push(tmpVariable);
        }
    }
    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Viewer_3D" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="OBJ File" nameLabel="Viewer_3D_property_obj_file" type="file"/>
		<property name="MTL File" nameLabel="Viewer_3D_property_mtl_file" type="file"/>
		<property name="Initial Rotation X" nameLabel="Viewer_3D_property_initial_rotation_x" type="string"/>
		<property name="Initial Rotation Y" nameLabel="Viewer_3D_property_initial_rotation_y" type="string"/>
		<property name="Initial Rotation Z" nameLabel="Viewer_3D_property_initial_rotation_z" type="string"/>
		<property name="Model Color" nameLabel="Viewer_3D_property_model_color" type="string"/>
		<property name="Background Color 1" nameLabel="Viewer_3D_property_background_color_1" type="string"/>
		<property name="Background Color 2" nameLabel="Viewer_3D_property_background_color_2" type="string"/>
		<property name="Render Mode" nameLabel="Viewer_3D_property_render_mode" type="{Smooth,Points,Wireframe,Flat}"/>
	</model>
<css>/* https://code.google.com/p/android/issues/detail?id=35474 */
div.Viewer_3D_frame {
    overflow: hidden;
    overflow-x: visible;
}

div.viewer-3d-wrapper {
    overflow: hidden;
    overflow-x: visible;
    -ms-touch-action: none;
    touch-action: none;
}

</css><view>&lt;div class="viewer-3d-wrapper"&gt;
    &lt;canvas class="3d-viewer-canvas"&gt;
    &lt;/canvas&gt;
&lt;/div&gt;
</view><preview>&lt;div class="viewer-3d-wrapper"&gt;
    &lt;canvas class="3d-viewer-canvas"&gt;
    &lt;/canvas&gt;
&lt;/div&gt;
</preview><presenter>function AddonViewer_3D_create(){
    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);

    var presenter = function () {};

    presenter.RENDER_MODES = {
        'Smooth': 'SMOOTH',
        'Points': 'POINTS',
        'Wireframe': 'WIREFRAME',
        'Flat': 'FLAT',
        DEFAULT: 'Smooth'
    };

    presenter.ERROR_CODES = {
        'ERR_01': 'Missing OBJ file!',
        'ERR_02': 'Invalid rotation values!',
        'ERR_03': 'Invalid model color!',
        'ERR_04': 'Invalid background color values!'
    };

    function deferredQueueDecoratorChecker () {
        return presenter.isLoaded;
    }

    presenter.getDeferredQueueVariable = function () {
        return deferredSyncQueue;
    };

    presenter.run = function (view, model) {
        presenter.presenterLogic(view, model);
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.createPreview = function (view, model) {
        presenter.presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    presenter.setCanvasDimensions = function (width, height) {
        presenter.$view.find('.3d-viewer-canvas').attr({
            width: width,
            height: height
        });
    };

    presenter.parseRotation = function (strRotation) {
        var rotation, parsedRotation;

        if (ModelValidationUtils.isStringEmpty(strRotation)) {
            rotation = 0;
        } else {
            parsedRotation = ModelValidationUtils.validateInteger(strRotation);

            if (!parsedRotation.isValid || parsedRotation.value &lt; 0) {
                return { isValid: false };
            } else {
                rotation = parsedRotation.value;
            }
        }

        return { isValid: true, rotation: rotation };
    };

    presenter.parseInitialRotation = function (model) {
        var strRotationX = model['Initial Rotation X'],
            parsedRotationX = presenter.parseRotation(strRotationX),
            strRotationY = model['Initial Rotation Y'],
            parsedRotationY = presenter.parseRotation(strRotationY),
            strRotationZ = model['Initial Rotation Z'],
            parsedRotationZ = presenter.parseRotation(strRotationZ);

        if (!parsedRotationX.isValid || !parsedRotationY.isValid || !parsedRotationZ.isValid) {
            return { isValid: false };
        }

        return { isValid: true, X: parsedRotationX.rotation, Y: parsedRotationY.rotation, Z: parsedRotationZ.rotation };
    };

    presenter.parseModelColor = function (modelColor) {
        if (ModelValidationUtils.isStringEmpty(modelColor)) {
            return { isValid: true, color: '#EEEEEE' };
        }

        return ModelValidationUtils.validateColor(modelColor, '#EEEEEE');
    };

    presenter.parseBackgroundColors = function (model) {
        var backgroundColor1 = model['Background Color 1'],
            parsedBackgroundColor1,
            backgroundColor2 = model['Background Color 2'],
            parsedBackgroundColor2;

        // Background Color 1
        if (ModelValidationUtils.isStringEmpty(backgroundColor1)) {
            parsedBackgroundColor1 = { color: "#CCCCCC" };
        } else {
            parsedBackgroundColor1 = ModelValidationUtils.validateColor(backgroundColor1, '#CCCCCC');
            if (!parsedBackgroundColor1.isValid) {
                return { isValid: false };
            }
        }

        // Background Color 1
        if (ModelValidationUtils.isStringEmpty(backgroundColor2)) {
            parsedBackgroundColor2 = { color: "#EEEEEE" };
        } else {
            parsedBackgroundColor2 = ModelValidationUtils.validateColor(backgroundColor2, '#EEEEEE');
            if (!parsedBackgroundColor2.isValid) {
                return { isValid: false };
            }
        }

        return {
            isValid: true,
            color1: parsedBackgroundColor1.color,
            color2: parsedBackgroundColor2.color
        };
    };

    presenter.parseModel = function (model) {
        if (ModelValidationUtils.isStringEmpty(model['OBJ File'])) {
            return { isValid: false, errorCode: 'ERR_01' };
        }

        var MTLFile = model['MTL File'];
        if (ModelValidationUtils.isStringEmpty(MTLFile)) {
            MTLFile = '';
        }

        var parsedInitialRotation = presenter.parseInitialRotation(model);
        if (!parsedInitialRotation.isValid) {
            return { isValid: false, errorCode: 'ERR_02' };
        }

        var parsedModelColor = presenter.parseModelColor(model['Model Color']);
        if (!parsedModelColor.isValid) {
            return { isValid: false, errorCode: 'ERR_03' };
        }

        var parsedBackgroundColors = presenter.parseBackgroundColors(model);
        if (!parsedBackgroundColors.isValid) {
            return { isValid: false, errorCode: 'ERR_04' };
        }

        var renderMode = ModelValidationUtils.validateOption(presenter.RENDER_MODES, model['Render Mode']);
        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);

        return {
            isValid: true,
            addonID: model.ID,
            isVisible: isVisible,
            isCurrentlyVisible: isVisible,
            files: {
                OBJ: model['OBJ File'],
                MTL: MTLFile
            },
            initialRotation: {
                X: parsedInitialRotation.X,
                Y: parsedInitialRotation.Y,
                Z: parsedInitialRotation.Z
            },
            colors: {
                model: parsedModelColor.color,
                background1: parsedBackgroundColors.color1,
                background2: parsedBackgroundColors.color2
            },
            renderMode: renderMode,
            quality: 'standard',
            queues: {
                X: {
                    name: model.ID + "_X",
                    isActive: false,
                    delay: 0,
                    angle: 0
                },
                Y: {
                    name: model.ID + "_Y",
                    isActive: false,
                    delay: 0,
                    angle: 0
                },
                Z: {
                    name: model.ID + "_Z",
                    isActive: false,
                    delay: 0,
                    angle: 0
                }
            }
        };
    };

    presenter.setViewerRenderMode = function (viewer) {
        var renderMode = "smooth";

        switch (presenter.configuration.renderMode) {
            case 'POINTS':
                renderMode = "point";
                break;
            case 'WIREFRAME':
                renderMode = "wireframe";
                break;
            case 'FLAT':
                renderMode = 'flat';
                break;
        }

        viewer.setParameter('RenderMode', renderMode);
    };

    presenter.renderObject = function () {
        var canvas =  presenter.$view.find('.3d-viewer-canvas')[0],
            viewer = new JSC3D.Viewer(canvas);

        presenter.viewer = viewer;
        viewer.setParameter('SceneUrl', presenter.configuration.files.OBJ + '.obj');
        viewer.setParameter('MtlLibUrl', presenter.configuration.files.MTL);
        viewer.setParameter('InitRotationX', presenter.configuration.initialRotation.X);
        viewer.setParameter('InitRotationY', presenter.configuration.initialRotation.Y);
        viewer.setParameter('InitRotationZ', presenter.configuration.initialRotation.Z);
        viewer.setParameter('ModelColor', presenter.configuration.colors.model);
        viewer.setParameter('BackgroundColor1', presenter.configuration.colors.background1);
        viewer.setParameter('BackgroundColor2', presenter.configuration.colors.background2);
        presenter.setViewerRenderMode(viewer);

        viewer.init();

        $.when(viewer.loadPromise).then(function () {
            presenter.isLoaded = true;

            deferredSyncQueue.resolve()
        });
    };

    presenter.presenterLogic = function (view, model) {
        presenter.$view = $(view);
        presenter.model = model;
        presenter.isLoaded = false;

        presenter.setCanvasDimensions(model.Width, model.Height);

        presenter.configuration = presenter.parseModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }

        presenter.renderObject();

        presenter.$view.click(function(e){
            e.stopPropagation();
        });
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'reset': presenter.reset,
            'show': presenter.show,
            'hide': presenter.hide,
            'rotateX': presenter.rotateXCommand,
            'rotateY': presenter.rotateYCommand,
            'rotateZ': presenter.rotateZCommand,
            'setState': presenter.setStateCommand,
            'setQuality': presenter.setQualityCommand,
            'startRotationX': presenter.startRotationXCommand,
            'stopRotationX': presenter.stopRotationX,
            'startRotationY': presenter.startRotationYCommand,
            'stopRotationY': presenter.stopRotationY,
            'startRotationZ': presenter.startRotationZCommand,
            'stopRotationZ': presenter.stopRotationZ,
            'stopAllRotations': presenter.stopAllRotations
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.rotateObject = function (angleX, angleY, angleZ) {
        presenter.viewer.rotate(angleX, angleY, angleZ);
        presenter.viewer.update();
    };

    presenter.validateAngle = function (angle) {
        var validatedAngle = ModelValidationUtils.validateFloat(angle);

        if (!validatedAngle.isValid) return { isValid: false };
        if (validatedAngle.value &lt; 0) return { isValid: false };

        return { isValid: true, value: validatedAngle.value };
    };

    presenter.rotateX = deferredSyncQueue.decorate(function (angle) {
        var validatedAngle = presenter.validateAngle(angle);
        if (!validatedAngle.isValid) return;

        presenter.rotateObject(validatedAngle.value, 0, 0);
    });

    presenter.rotateXCommand = function (params) {
        presenter.rotateX(params[0]);
    };

    presenter.rotateY = deferredSyncQueue.decorate(function (angle) {
        var validatedAngle = presenter.validateAngle(angle);
        if (!validatedAngle.isValid) return;

        presenter.rotateObject(0, validatedAngle.value, 0);
    });

    presenter.rotateYCommand = function (params) {
        presenter.rotateY(params[0]);
    };

    presenter.rotateZ = deferredSyncQueue.decorate(function (angle) {
        var validatedAngle = presenter.validateAngle(angle);
        if (!validatedAngle.isValid) return;

        presenter.rotateObject(0, 0, validatedAngle.value);
    });

    presenter.rotateZCommand = function (params) {
        presenter.rotateZ(params[0]);
    };

    presenter.setQuality = deferredSyncQueue.decorate(function (quality) {
        if (ModelValidationUtils.isStringEmpty(quality)) return;
        if (quality !== 'low' &amp;&amp; quality !== 'standard' &amp;&amp; quality !== 'high') return;
        if (presenter.configuration.quality === quality) return;

        presenter.configuration.quality = quality;
        presenter.viewer.setDefinition(quality);
        presenter.viewer.update();
    });

    presenter.setQualityCommand = function (params) {
        presenter.setQuality(params[0]);
    };

    presenter.reset = deferredSyncQueue.decorate(function () {
        presenter.stopAllRotations();
        presenter.setQuality('standard');

        presenter.setVisibility(presenter.configuration.isVisible);
        presenter.configuration.isCurrentlyVisible = presenter.configuration.isVisible;

        presenter.viewer.replaceSceneFromUrl(presenter.configuration.files.OBJ + '.obj');
        presenter.viewer.update();
    });

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = deferredSyncQueue.decorate(function () {
        if (presenter.configuration.isCurrentlyVisible) return;

        presenter.configuration.isCurrentlyVisible = true;
        presenter.setVisibility(true);
    });

    presenter.hide = deferredSyncQueue.decorate(function () {
        if (!presenter.configuration.isCurrentlyVisible) return;

        presenter.configuration.isCurrentlyVisible = false;
        presenter.setVisibility(false);
    });

    presenter.getState = function () {
        if (!presenter.isLoaded) return;

        return JSON.stringify({
            isVisible: presenter.configuration.isCurrentlyVisible
        });
    };

    presenter.setStateCommand = function (params) {
        presenter.setState(params[0]);
    };

    presenter.setState = deferredSyncQueue.decorate(function (state) {
        if (!state) return;

        var parsedState = JSON.parse(state);

        if (parsedState.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
    });

    presenter.validateDelay = function (delay) {
        var validatedDelay = ModelValidationUtils.validateInteger(delay);

        if (!validatedDelay.isValid) return { isValid: false };
        if (validatedDelay.value &lt; 0) return { isValid: false };

        return { isValid: true, value: validatedDelay.value };
    };

    // Generic commands

    presenter.startRotation = deferredSyncQueue.decorate(function (axis, angle, delay) {
        var validatedAngle = presenter.validateAngle(angle);
        if (!validatedAngle.isValid) return;

        var validatedDelay = presenter.validateDelay(delay);
        if (!validatedDelay.isValid) return;

        if (presenter.configuration.queues[axis].isActive) {
            if (validatedDelay.value == 0) {
                presenter['stopRotation' + axis]();
                return;
            }

            presenter.configuration.queues[axis].delay = validatedDelay.value;
            presenter.configuration.queues[axis].angle = validatedAngle.value;
        } else {
            presenter.configuration.queues[axis].isActive = true;
            presenter.configuration.queues[axis].delay = validatedDelay.value;
            presenter.configuration.queues[axis].angle = validatedAngle.value;

            presenter['startRotation' + axis + 'Queue']();
        }
    });

    presenter.startRotationQueue = function (axis) {
        var queue = presenter.configuration.queues[axis].name,
            delay = presenter.configuration.queues[axis].delay;

        $.doTimeout(queue, delay, function () {
            var angle = presenter.configuration.queues[axis].angle,
                angleX = 0, angleY = 0, angleZ = 0;

            switch (axis) {
                case 'X':
                    angleX = angle;
                    break;
                case 'Y':
                    angleY = angle;
                    break;
                case 'Z':
                    angleZ = angle;
                    break;
            }

            presenter.rotateObject(angleX, angleY, angleZ);

            return true; // continue callback call
        });
    };

    presenter.stopRotation = deferredSyncQueue.decorate(function (axis) {
        if (!presenter.configuration.queues[axis].isActive) return;

        presenter.configuration.queues[axis].isActive = false;
        presenter.configuration.queues[axis].angle = 0;
        presenter.configuration.queues[axis].delay = 0;

        presenter['stopRotation' + axis + 'Queue']();
    });

    presenter.stopRotationQueue = function (axis) {
        var queue = presenter.configuration.queues[axis].name;

        $.doTimeout(queue);
    };

    // X-axis specific rotation commands

    presenter.startRotationX = function (angle, delay) {
        presenter.startRotation('X', angle, delay);
    };

    presenter.startRotationXCommand = function (params) {
        presenter.startRotationX(params[0], params[1]);
    };

    presenter.startRotationXQueue = function () {
        presenter.startRotationQueue('X');
    };

    presenter.stopRotationX = function () {
        presenter.stopRotation('X');
    };

    presenter.stopRotationXQueue = function () {
        presenter.startRotationQueue('X');
    };

    // Y-axis specific rotation commands

    presenter.startRotationY = function (angle, delay) {
        presenter.startRotation('Y', angle, delay);
    };

    presenter.startRotationYCommand = function (params) {
        presenter.startRotationY(params[0], params[1]);
    };

    presenter.startRotationYQueue = function () {
        presenter.startRotationQueue('Y');
    };

    presenter.stopRotationY = function () {
        presenter.stopRotation('Y');
    };

    presenter.stopRotationYQueue = function () {
        presenter.startRotationQueue('Y');
    };

    // Z-axis specific rotation commands

    presenter.startRotationZ = function (angle, delay) {
        presenter.startRotation('Z', angle, delay);
    };

    presenter.startRotationZCommand = function (params) {
        presenter.startRotationZ(params[0], params[1]);
    };

    presenter.startRotationZQueue = function () {
        presenter.startRotationQueue('Z');
    };

    presenter.stopRotationZ = function () {
        presenter.stopRotation('Z');
    };

    presenter.stopRotationZQueue = function () {
        presenter.startRotationQueue('Z');
    };

    presenter.stopAllRotations = function () {
        presenter.stopRotationX();
        presenter.stopRotationY();
        presenter.stopRotationZ();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Vimeo" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="movie id" nameLabel="Vimeo_property_movie_id" type="string"/>
	</model>
<css/><view>&lt;iframe 
	src=''
	width='100%' 
	height='100%' 
	frameborder="0" 
	webkitAllowFullScreen 
	mozallowfullscreen 
	allowFullScreen&gt; 
&lt;/iframe&gt;
</view><preview>&lt;img src="" style="width:100%;height:100%"/&gt;
</preview><presenter>function AddonVimeo_create(){
			
	var presenter = function(){};

	const ERROR_MESSAGES = {
		INVALID_ID: "Invalid movie ID"
	};

	presenter.run = function(view, model){
		const src = `https://player.vimeo.com/video/${model["movie id"]}?title=0&amp;byline=0&amp;portrait=0`;
		const sanitizedSrc = window.xssUtils.sanitize(src);
		
		const iframe = view.getElementsByTagName("iframe")[0];
		iframe.setAttribute("src", sanitizedSrc);
	};
	
	presenter.createPreview = function(view, model){
		const src = `https://vimeo.com/api/v2/video/${model["movie id"]}.json`;
		const sanitizedSrc = window.xssUtils.sanitize(src);
		
		fetch(sanitizedSrc, {method: "GET"})
			.then(response =&gt; response.json())
			.then(data =&gt; {
				const thumbnailUrl = data[0].thumbnail_large;
				$(view).children("img:first").attr("src", thumbnailUrl);
			}).catch(error =&gt; {
				showErrorMessage(view);
			});
	};
	
	function showErrorMessage (view) {
		$(view).html(`&lt;p&gt;${ERROR_MESSAGES.INVALID_ID}&lt;/p&gt;`);
	}
	
	return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Visual_Feedback_Creator" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="ScriptBlocks" nameLabel="Advanced_Connector_property_scripts" type="connectorblocks"/>
        <property name="Is disabled" nameLabel="Advanced_Connector_property_is_disabled" type="boolean"/>
	</model>
<css/><view/><preview/><presenter>function AddonVisual_Feedback_Creator_create() {
    var presenter = function () {};
    var event;

    presenter.STANDARD_EVENTS = [
        'ValueChanged',
        'Definition',
        'ItemSelected',
        'ItemConsumed',
        'ItemReturned',
        'PageLoaded',
        'PageAllOk',
        'ShowAnswers',
        'HideAnswers',
        'Done',
        'AllAttempted',
        'NotAllAttempted',
        'LimitedCheck'
    ];

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
    };

    presenter.onEventReceived = function (eventName, eventData) {
        if (presenter.isDisabled) {
            return;
        }
        var i, length;
        event = presenter.fillEventData(eventData, eventName);

        try {
            var filteredEvents = presenter.filterEvents(presenter.events, event);
            for (i = 0, length = filteredEvents.length; i &lt; length; i++) {
                eval(filteredEvents[i].Code);
            }
        } catch (error) {
            Helpers.alertErrorMessage(error, "Advanced Connector - problem occurred while running scripts!");
        }

        event = undefined;
    };

    presenter.reset = function () {
        presenter.onEventReceived('Reset', {});
    };

    presenter.setShowErrorsMode = function () {
        presenter.onEventReceived('Check', {});
    };

    presenter.setWorkMode = function () {
        presenter.onEventReceived('Uncheck', {});
    };

    function isCustomEvent(eventName) {
        return $.inArray(eventName, presenter.STANDARD_EVENTS) == -1;
    }

    presenter.run = function(view, model) {
        var scripts = presenter.blocklyToCode(model.ScriptBlocks);
        var validatedScript = presenter.validateScript(scripts), eventBus,
            customEventListeners = [];

        if (validatedScript.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedScript.errorCode);
            return;
        }

        presenter.isDisabled = ModelValidationUtils.validateBoolean(model["Is disabled"]);
        if (presenter.isDisabled) {
            return;
        }

        eventBus = presenter.playerController.getEventBus();
        presenter.events = validatedScript.events;

        $.each(presenter.STANDARD_EVENTS, function(_, name) {
            eventBus.addEventListener(name, presenter);
        });

        $.each(presenter.events, function() {
            if (isCustomEvent(this.Name) &amp;&amp; (customEventListeners.indexOf(this.Name) == -1) ){
                eventBus.addEventListener(this.Name.trim(), presenter);
                customEventListeners.push(this.Name);
            }
        });

        $(view).css('visibility', 'hidden');
    };

    presenter.blocklyToCode = function(blocks_text) {
        window.BlocklyCustomBlocks.AGC.addBlocks();
        try {
            var xml = Blockly.Xml.textToDom(blocks_text)
        } catch (e) {
            alert(e);
            return;
        }
        var workspace = new Blockly.Workspace();
        Blockly.Xml.domToWorkspace(xml, workspace);
        var code = Blockly.JavaScript.workspaceToCode(workspace);
        return code;
    };

    presenter.checkScriptsResources = function(script) {
        script = script.replace(/\s/g,'');
        var regex = new RegExp("[\(|\=](\'|\")*(/)*file/serve/[0-9]*");

        return regex.test(script);
    };

    presenter.createPreview = function(view, model) {
        var scripts = presenter.blocklyToCode(model.ScriptBlocks);
        var validatedScript = presenter.validateScript(scripts);
        if (validatedScript.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedScript.errorCode);
        }
        if (presenter.checkScriptsResources(scripts)) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, 'SV_09');
        }

    };

    presenter.ERROR_CODES = {
        'SV_01': "Script source must be provided and cannot be empty!",
        'SV_02': "All blocks must be contained within a source module block!",
        'SV_03': "All blocks must be contained within a source module block!",
        'SV_04': "Missing SCRIPTSTART keyword or new line after it!",
        'SV_05': "Missing SCRIPTEND keyword or new line after it!",
        'SV_06': "Repeated event field value declaration!",
        'SV_07': "Repeated keyword!",
        'SV_08': "Invalid identification. Should be Source,Item, Value or Score!",
        'SV_09': "Please pay attention to the correct linking resources"
    };

    function returnErrorResult(errorCode) {
        return { isError: true, errorCode: errorCode };
    }

    presenter.fillBlankFields = function(eventDeclaration) {
        for (var prop in eventDeclaration) {
            if (eventDeclaration.hasOwnProperty(prop) &amp;&amp; prop !== 'Name') {
                if (eventDeclaration[prop] == undefined) {
                    eventDeclaration[prop] = '.*';
                }
            }
        }

        if (eventDeclaration.Name == undefined) {
            eventDeclaration.Name = 'ValueChanged';
        }
    };

    presenter.findKeywordIndex = function(script, keyword, currentLine) {
        for (var i = currentLine + 1; i &lt; script.length; i++) {
            if (script[i].trim() === keyword) return i;
        }

        return -1;
    };

    presenter.validateEvent = function(script) {
        var indexes = [], i, length;
        var eventDeclaration = {
            Source: undefined,
            Item: undefined,
            Value: undefined,
            Score: undefined,
            Name: undefined,
            Word: undefined,
            Type: undefined,
            Code: ''
        };

        for(i = 0, length = script.length; i &lt; length; i++) {
            indexes[i] = false;
        }

        var scriptStartIndex = presenter.findKeywordIndex(script, "SCRIPTSTART", -1);
        var scriptEndIndex = presenter.findKeywordIndex(script, "SCRIPTEND", 1);

        var isScriptInvalid = scriptStartIndex === -1 &amp;&amp; scriptEndIndex !== -1;
        isScriptInvalid = isScriptInvalid || scriptStartIndex !== -1 &amp;&amp; scriptEndIndex === -1;
        isScriptInvalid = isScriptInvalid || scriptStartIndex &gt; scriptEndIndex;

        if (isScriptInvalid) return returnErrorResult('SV_04');

        indexes[scriptStartIndex] = true;
        indexes[scriptEndIndex] = true;

        for (i = scriptStartIndex + 1; i &lt; scriptEndIndex; i++) {
            indexes[i] = true;
            eventDeclaration.Code += script[i];
            if (i !== scriptEndIndex - 1) {
                eventDeclaration.Code += '\n';
            }
        }

        for(i = 0, length = indexes.length; i &lt; length; i++) {
            if (indexes[i]) continue;

            var line = script[i].split(':');
            if (line.length !== 2) return returnErrorResult('SV_08');

            var keywords = ['Source', 'Item', 'Value', 'Score', 'Name', 'Type', 'Word'];

            var trimmedKeyword = line[0].trim();
            if (keywords.indexOf(trimmedKeyword) === -1) return returnErrorResult('SV_08');

            if (eventDeclaration[trimmedKeyword]) {
                return returnErrorResult('SV_06');
            } else {
                eventDeclaration[trimmedKeyword] = line[1];
            }
        }

        presenter.fillBlankFields(eventDeclaration);

        return { isError: false, eventDeclaration: eventDeclaration };
    };

    function extractLines(script, start, end) {
        var array = [];

        for (var i = start; i &lt; end; i++) array.push(script[i]);

        return array;
    }

    function cleanScriptFromEmptyLines(script) {
        var array = [];

        for (var i = 0; i &lt; script.length; i++) {
            if (script[i]) array.push(script[i]);
        }

        return array;
    }

    presenter.validateScript = function (script) {
        if (!script) return returnErrorResult('SV_01');

        var scriptsArray = [];
        var scriptLines = cleanScriptFromEmptyLines(script.split('\n'));
        var lineIndex = 0, length = scriptLines.length;

        while (lineIndex &lt; length) {
            if (scriptLines[lineIndex] !== "EVENTSTART") return returnErrorResult('SV_02');

            var endEventIndex = presenter.findKeywordIndex(scriptLines, "EVENTEND", lineIndex);
            if (endEventIndex === -1) return returnErrorResult('SV_03');

            lineIndex++;
            var startEventIndex = presenter.findKeywordIndex(scriptLines, "EVENTSTART", lineIndex);

            if (startEventIndex !== -1 &amp;&amp; startEventIndex &lt; endEventIndex) return returnErrorResult('SV_07');

            var eventCode = extractLines(scriptLines, lineIndex, endEventIndex);
            var validatedEvent = presenter.validateEvent(eventCode);
            if (validatedEvent.isError) {
                return returnErrorResult(validatedEvent.errorCode);
            } else {
                scriptsArray.push(validatedEvent.eventDeclaration);
            }

            lineIndex = endEventIndex + 1;
        }

        return { isError: false, events: scriptsArray };
    };

    presenter.matchFieldToRule = function (field, rule, doExactMatch) {
        if (rule.length &gt; 0)
            var rule = doExactMatch ? '^' + $.trim(rule) + '$' : $.trim(rule);
        return new RegExp(rule).test(field);
    };

    presenter.filterEvents = function (events, event) {
        var filteredArray = [], isMatch;

        try {
            for (var i = 0, length = events.length; i &lt; length; i++) {
                isMatch = presenter.matchFieldToRule(event.name, events[i].Name, true);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.source, events[i].Source);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.item, events[i].Item);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.value, events[i].Value);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.score, events[i].Score);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.word, events[i].Word);
                isMatch = isMatch &amp;&amp; presenter.matchFieldToRule(event.type, events[i].Type);

                if (isMatch) {
                    filteredArray.push(events[i]);
                }
            }
        } catch (error) {
            Helpers.alertErrorMessage(error, "Advanced Connector - problem occurred while processing conditions!");
        }

        return filteredArray;
    };

    presenter.fillEventData = function (eventData, eventName) {
        var filledEventData = $.extend(true, {}, eventData);
        filledEventData.name = eventName;

        if (!filledEventData.word) filledEventData.word = '';
        if (!filledEventData.type) filledEventData.type = '';
        if (!filledEventData.item) filledEventData.item = '';
        if (!filledEventData.value) filledEventData.value = '';
        if (!filledEventData.source) filledEventData.source = '';
        if (!filledEventData.score) filledEventData.score = '';

        return filledEventData;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="WritingCalculations" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property isDefault="true" name="Value" nameLabel="WritingCalculations_property_value" type="text"/>
        <property name="Signs" nameLabel="WritingCalculations_property_signs" type="list">
            <property name="Addition" nameLabel="WritingCalculations_property_addition" type="string"/>
            <property name="Subtraction" nameLabel="WritingCalculations_property_subtraction" type="string"/>
            <property name="Division" nameLabel="WritingCalculations_property_division" type="string"/>
            <property name="Multiplication" nameLabel="WritingCalculations_property_multiplication" type="string"/>
            <property name="Equals" nameLabel="WritingCalculations_property_equals" type="string"/>
        </property>
        <property displayName="Styles" name="Styles" nameLabel="WritingCalculations_property_styles" type="list">
            <property name="Column" nameLabel="WritingCalculations_property_column" type="string"/>
            <property name="Row" nameLabel="WritingCalculations_property_row" type="string"/>
            <property name="Class" nameLabel="WritingCalculations_property_class" type="string"/>
            <property name="Style" nameLabel="WritingCalculations_property_style" type="string"/>
        </property>
        <property displayName="Commutativity" name="Commutativity" nameLabel="WritingCalculations_property_commutativity" type="boolean"/>
        <property displayName="Is not activity" name="Is not activity" nameLabel="WritingCalculations_property_is_not_activity" type="boolean"/>
        <property displayName="Enable more digits in gap" name="Multisigns" nameLabel="WritingCalculations_property_is_not_activity" type="boolean"/>
        <property displayName="Use numeric keyboard" name="useNumericKeyboard" nameLabel="WritingCalculations_use_numeric_keyboard" type="boolean"/>
        <property name="showAllAnswersInGSA" nameLabel="WritingCalculations_show_all_answers_in_GSA" type="boolean"/>
        <property displayName="Description of operation" name="DescriptionOfOperation" nameLabel="WritingCalculations_description_of_operation" type="string"/>
        <property displayName="Lang attribute" name="LangTag" nameLabel="WritingCalculations_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="WritingCalculations_speech_texts" type="staticlist">

            <property name="Gap" nameLabel="WritingCalculations_gap" type="staticrow">
                <property name="Gap" nameLabel="WritingCalculations_gap" type="string"/>
            </property>

            <property name="AdditionalGap" nameLabel="WritingCalculations_additional_gap" type="staticrow">
                <property name="AdditionalGap" nameLabel="WritingCalculations_additional_gap" type="string"/>
            </property>

            <property name="Empty" nameLabel="WritingCalculations_empty" type="staticrow">
                <property name="Empty" nameLabel="WritingCalculations_empty" type="string"/>
            </property>

            <property name="AdditionSign" nameLabel="WritingCalculations_addition_sign" type="staticrow">
                <property name="AdditionSign" nameLabel="WritingCalculations_addition_sign" type="string"/>
            </property>

            <property name="SubtractionSign" nameLabel="WritingCalculations_subtraction_sign" type="staticrow">
                <property name="SubtractionSign" nameLabel="WritingCalculations_subtraction_sign" type="string"/>
            </property>

            <property name="DivisionSign" nameLabel="WritingCalculations_division_sign" type="staticrow">
                <property name="DivisionSign" nameLabel="WritingCalculations_division_sign" type="string"/>
            </property>

            <property name="MultiplicationSign" nameLabel="WritingCalculations_multiplication_sign" type="staticrow">
                <property name="MultiplicationSign" nameLabel="WritingCalculations_multiplication_sign" type="string"/>
            </property>

            <property name="EqualsSign" nameLabel="WritingCalculations_equals_sign" type="staticrow">
                <property name="EqualsSign" nameLabel="WritingCalculations_equals_sign" type="string"/>
            </property>

            <property name="DecimalSeparator" nameLabel="WritingCalculations_decimal_separator" type="staticrow">
                <property name="DecimalSeparator" nameLabel="WritingCalculations_decimal_separator" type="string"/>
            </property>

            <property name="Line" nameLabel="WritingCalculations_line" type="staticrow">
                <property name="Line" nameLabel="WritingCalculations_line" type="string"/>
            </property>

            <property name="Correct" nameLabel="WritingCalculations_correct" type="staticrow">
                <property name="Correct" nameLabel="WritingCalculations_correct" type="string"/>
            </property>

            <property name="Wrong" nameLabel="WritingCalculations_wrong" type="staticrow">
                <property name="Wrong" nameLabel="WritingCalculations_wrong" type="string"/>
            </property>

        </property>
        <property displayName="Use alternative TTS navigation" name="UseAlternativeTTSNavigation" nameLabel="WritingCalculations_property_use_alternative_tts_navigation" type="boolean"/>
        <property displayName="Rows' alt texts" name="RowsAltTexts" nameLabel="WritingCalculations_property_rows_alt_texts" type="list">
            <property name="AltText" nameLabel="WritingCalculations_property_rows_alt_texts_alt_text" type="string"/>
        </property>
    </model>
<css>.wrapper-cell, .writing-calculations-input {
    float: left;
    width: 50px;
    height: 25px;
    line-height: 25px;
    padding: 0;
    text-align: center;
    font-size: 12px;
    margin: 1px 0;
}

.writing-calculations-input {
    width: 25px;
    float: none;
    height: 23px;
    text-align: center !important;
}

.wrapper-row {
    float: left;
}

.writing-calculations-input.incorrect {
    border: 1px solid red !important;
    background-color: #faa !important;
}

.writing-calculations-input.correct {
    border: 1px solid green !important;
    background-color: #afa !important;
}

.wrapper-line {
    float: left;
    width: 50px;
}

.container-line {
    border-bottom: 1px solid #111;
    height: 5px;
}

.wrapper-empty-space {
    float: left;
    height: 1px;
    width: 50px;
}

.wrapper-dot {
    float: left;
    width: 0px;
    position: relative;
}

.container-dot {
    position: absolute;
    left: -4px;
    top: 4px;
}

.wrapper-cell {
    min-height: 3px;
}

.wrong {
    border: 1px solid #ff1111;
}

.correct {
    border: 1px solid #00bb00;
}

.container-helpBox .writing-calculations-input {
    background-color: lightslategray;
    width: 20px;
    height: 20px;
}
</css><view>&lt;div id="writing-calculations-wrapper"&gt;&lt;/div&gt;
</view><preview>&lt;div id="writing-calculations-wrapper"&gt;&lt;/div&gt;
</preview><presenter>function AddonWritingCalculations_create() {
    var presenter = function () {};

    presenter.$view = null;
    presenter.model = null;
    presenter.correctAnswersList = [];
    presenter.playerController = null;
    presenter.answers = [];
    presenter.isCommutativity;
    presenter.useNumericKeyboard = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.isScoreSaved = false;
    presenter.userAnswers = [];
    presenter.helpBoxesDefaultValues = [];
    presenter.helpBoxesUserAnswers = [];
    presenter.isDisabled = false;
    presenter.isWCAGOn = false;
    presenter.tableElements = [];
    presenter.rowsAltTexts = [];
    presenter.gapElements = [];
    presenter.rowsStatusesOfNavigabilityInAltTTSNavigation = [];
    presenter.isGapFocused = false;
    var eventBus;

    const DESCRIPTION_OF_OPERATION_ROW_INDEX = -1;
    const DESCRIPTION_OF_OPERATION_COLUMN_INDEX = 0;
    const ALT_TEXT_FOR_ROW_COLUMN_INDEX = -1;
    const NO_GAP_INDEX = -1;

    presenter.keyboardState = {
        x: DESCRIPTION_OF_OPERATION_COLUMN_INDEX,
        y: DESCRIPTION_OF_OPERATION_ROW_INDEX,
        gapIndex: NO_GAP_INDEX
    };

    presenter.KEYBOARD_SELECTED_CLASS = 'keyboard_navigation_active_element';

    presenter.ELEMENT_TYPE = {
        "NUMBER" : 1,        // Non box numbers
        "EMPTY_BOX" : 2,
        "SYMBOL" : 3,
        "EMPTY_SPACE" : 4,
        "LINE" : 5,
        "DOT" : 6,
        "HELP_BOX" : 7,
    };

    presenter.upgradeModel = function (model) {
        let upgradedModel = presenter.upgradeSigns(model);
        upgradedModel = presenter.upgradeNumericKeyboard(upgradedModel);
        upgradedModel = presenter.upgradeModelAddShowAllAnswersInGSA(upgradedModel);
        upgradedModel = presenter.upgradeTTS(upgradedModel);
        return presenter.upgradeModelWithAlternativeTTSNavigation(upgradedModel);
    };

    presenter.upgradeModelAddShowAllAnswersInGSA = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('showAllAnswersInGSA')) {
            upgradedModel['showAllAnswersInGSA'] = 'False';
        }

        return upgradedModel;
    };

    presenter.upgradeModelWithAlternativeTTSNavigation = function (model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty("UseAlternativeTTSNavigation")) {
            upgradedModel["UseAlternativeTTSNavigation"] = "False";
        }

        if (!upgradedModel.hasOwnProperty("RowsAltTexts")) {
            upgradedModel["RowsAltTexts"] = [{"AltText": ""}];
        }

        return upgradedModel;
    };

    presenter.upgradeSigns = function (model) {
        if ('Signs' in model) return model;

        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object
        upgradedModel['Signs'] = [{
            'Addition' : '',
            'Subtraction' : '',
            'Division' : '',
            'Multiplication' : '',
            'Equals' : ''
        }];
        return upgradedModel;
    };

    presenter.upgradeTTS = function (model) {
        if ('speechTexts' in model) return model;

        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Gap"]) {
            upgradedModel["speechTexts"]["Gap"] = {Gap: ""};
        }
        if (!upgradedModel["speechTexts"]["AdditionalGap"]) {
            upgradedModel["speechTexts"]["AdditionalGap"] = {AdditionalGap: ""};
        }
        if (!upgradedModel["speechTexts"]["Empty"]) {
            upgradedModel["speechTexts"]["Empty"] = {Empty: ""};
        }
        if (!upgradedModel["speechTexts"]["AdditionSign"]) {
            upgradedModel["speechTexts"]["AdditionSign"] = {AdditionSign: ""};
        }
        if (!upgradedModel["speechTexts"]["SubtractionSign"]) {
            upgradedModel["speechTexts"]["SubtractionSign"] = {SubtractionSign: ""};
        }
        if (!upgradedModel["speechTexts"]["DivisionSign"]) {
            upgradedModel["speechTexts"]["DivisionSign"] = {DivisionSign: ""};
        }
        if (!upgradedModel["speechTexts"]["MultiplicationSign"]) {
            upgradedModel["speechTexts"]["MultiplicationSign"] = {MultiplicationSign: ""};
        }
        if (!upgradedModel["speechTexts"]["EqualsSign"]) {
            upgradedModel["speechTexts"]["EqualsSign"] = {EqualsSign: ""};
        }
        if (!upgradedModel["speechTexts"]["DecimalSeparator"]) {
            upgradedModel["speechTexts"]["DecimalSeparator"] = {DecimalSeparator: ""};
        }
        if (!upgradedModel["speechTexts"]["Line"]) {
            upgradedModel["speechTexts"]["Line"] = {Line: ""};
        }
        if (!upgradedModel["speechTexts"]["Correct"]) {
            upgradedModel["speechTexts"]["Correct"] = {Correct: ""};
        }
        if (!upgradedModel["speechTexts"]["Wrong"]) {
            upgradedModel["speechTexts"]["Wrong"] = {Wrong: ""};
        }

        if (!upgradedModel["DescriptionOfOperation"]) {
            upgradedModel["DescriptionOfOperation"] = "";
        }

        if (!upgradedModel["LangTag"]) {
            upgradedModel["LangTag"] = "";
        }

        return upgradedModel;
    };

    presenter.ERROR_MESSAGES = {
        V01 : 'Error in row number %rowIndex%. Missing closing square bracket.',
        V02 : 'Error in row number %rowIndex%. Missing opening square bracket.',
        V03 : 'Error in row number %rowIndex%. Missing number between square brackets.',
        V04 : 'Error in row number %rowIndex%. Number between brackets must be from 0 to 9.',
        V05 : 'Error in row number %rowIndex%. Given value "%value%" in square brackets is not a valid number.',
        V06 : 'Error in row number %rowIndex%. Missing closing curly bracket.',
        V07 : 'Error in row number %rowIndex%. Missing opening curly bracket.',
        V08 : 'Error in row number %rowIndex%. Given value "%value%" in curly brackets is not a valid. Curly brackets must be empty or have a valid number.',
        RAT01 : "Number of rows' alt texts (%rowsAltTextsNumber%) is not equal to number of navigable rows in alternative TTS navigation (%navigableRowsNumber%)."
    };

    presenter.run = function(view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(presenter.isVisible);
        eventBus = presenter.playerController.getEventBus();
        eventBus.addEventListener('ShowAnswers', this);
        eventBus.addEventListener('HideAnswers', this);
        eventBus.addEventListener('GradualShowAnswers', this);
        eventBus.addEventListener('GradualHideAnswers', this);
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model);
        presenter.setVisibility(true);
    };

    presenter.upgradeNumericKeyboard = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model.useNumericKeyboard === undefined) {
            upgradedModel["useNumericKeyboard"] = "False";
        }

        return upgradedModel;
    };

    function presenterLogic(view, model) {
        model = presenter.upgradeModel(model);
        presenter.isCommutativity = ModelValidationUtils.validateBoolean(model['Commutativity']) || false;
        presenter.$view = $(view);
        presenter.model = presenter.upgradeModel(model);
        presenter.signs = presenter.readSigns( presenter.model['Signs'][0] );
        presenter.isNotActivity = ModelValidationUtils.validateBoolean(model['Is not activity']);
        presenter.useNumericKeyboard = ModelValidationUtils.validateBoolean(model['useNumericKeyboard']);
        presenter.multisigns = ModelValidationUtils.validateBoolean(model['Multisigns']);
        presenter.isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.isVisibleByDefault = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.ID = model["ID"];
        presenter.showAllAnswersInGSA = ModelValidationUtils.validateBoolean(model['showAllAnswersInGSA']);
        presenter.langTag = model["LangTag"];
        presenter.descriptionOfOperation = model["DescriptionOfOperation"];
        presenter.setSpeechTexts(model["speechTexts"]);

        const validatedModelValue = presenter.validateModelValue(model['Value']);
        if (!validatedModelValue.isValid) {
            presenter.showErrorMessage(
                presenter.ERROR_MESSAGES[validatedModelValue.errorCode],
                validatedModelValue.errorMessageSubstitutions
            );
            return;
        }

        presenter.useAlternativeTTSNavigation = ModelValidationUtils.validateBoolean(model["UseAlternativeTTSNavigation"]);
        if (presenter.useAlternativeTTSNavigation) {
            createRowsStatuesOfNavigability(model["RowsAltTexts"], validatedModelValue.value);
            const validatedModelRowsAltTexts = presenter.validateRowsAltTexts(model["RowsAltTexts"]);
            if (!validatedModelRowsAltTexts.isValid) {
                presenter.showErrorMessage(
                    presenter.ERROR_MESSAGES[validatedModelRowsAltTexts.errorCode],
                    validatedModelRowsAltTexts.errorMessageSubstitutions
                );
                return;
            }
        }
        presenter.setAltTexts(model["RowsAltTexts"]);
        presenter.createView(validatedModelValue.value);
        presenter.bindValueChangeEvent();
        presenter.setContainerWidth();
        presenter.addAdditionalStyles();
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            gap: 'gap',
            additionalGap: 'additional gap',
            empty: 'empty',
            additionSign: 'addition sign',
            subtractionSign: 'subtraction sign',
            divisionSign: 'division sign',
            multiplicationSign: 'multiplication sign',
            equalsSign: 'equals sign',
            decimalSeparator: 'decimal separator',
            line: 'line',
            correct: 'correct',
            wrong: 'wrong',

        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            gap: TTSUtils.getSpeechTextProperty(speechTexts['Gap']['Gap'], presenter.speechTexts.gap),
            additionalGap: TTSUtils.getSpeechTextProperty(speechTexts['AdditionalGap']['AdditionalGap'], presenter.speechTexts.additionalGap),
            empty: TTSUtils.getSpeechTextProperty(speechTexts['Empty']['Empty'], presenter.speechTexts.empty),
            additionSign: TTSUtils.getSpeechTextProperty(speechTexts['AdditionSign']['AdditionSign'], presenter.speechTexts.additionSign),
            subtractionSign: TTSUtils.getSpeechTextProperty(speechTexts['SubtractionSign']['SubtractionSign'], presenter.speechTexts.subtractionSign),
            divisionSign: TTSUtils.getSpeechTextProperty(speechTexts['DivisionSign']['DivisionSign'], presenter.speechTexts.divisionSign),
            multiplicationSign: TTSUtils.getSpeechTextProperty(speechTexts['MultiplicationSign']['MultiplicationSign'], presenter.speechTexts.multiplicationSign),
            equalsSign: TTSUtils.getSpeechTextProperty(speechTexts['EqualsSign']['EqualsSign'], presenter.speechTexts.equalsSign),
            decimalSeparator: TTSUtils.getSpeechTextProperty(speechTexts['DecimalSeparator']['DecimalSeparator'], presenter.speechTexts.decimalSeparator),
            line: TTSUtils.getSpeechTextProperty(speechTexts['Line']['Line'], presenter.speechTexts.line),
            correct: TTSUtils.getSpeechTextProperty(speechTexts['Correct']['Correct'], presenter.speechTexts.correct),
            wrong: TTSUtils.getSpeechTextProperty(speechTexts['Wrong']['Wrong'], presenter.speechTexts.wrong)
        };
    }

    presenter.setAltTexts = function(rowsAltTexts) {
        presenter.rowsAltTexts = []

        if (!rowsAltTexts) {
            return;
        }

        for (let rowIndex = 0; rowIndex &lt; rowsAltTexts.length; rowIndex++) {
            presenter.rowsAltTexts.push(rowsAltTexts[rowIndex].AltText);
        }
    }

    presenter.readSigns = function( signs ) {
        var properSigns = {};
        for ( var key in signs ) {
            if ( signs.hasOwnProperty(key) ) {
                if ( signs[key] == '' || signs[key] == '&lt;br&gt;' ) {
                    properSigns[key] = presenter.useDefaultSign( key );
                } else {
                    properSigns[key] = signs[key];
                }
            }
        }
        return properSigns;
    };

    presenter.useDefaultSign = function( key ) {
        if (key == 'Addition') {
            return "\\(+\\)";
        }
        if (key == 'Subtraction') {
            return "\\(-\\)";
        }
        if (key == 'Division') {
            return "\\(\\big)\\)";
        }
        if (key == 'Multiplication') {
            return "\\(\\times\\)";
        }
        if (key == 'Equals') {
            return "\\(=\\)";
        }
    };

    presenter.addAdditionalStyleToElement = function (row, column, style, clazz) {
        var rowElement = presenter.$view.find('.row-' + row),
            cellElement = rowElement.find('.cell-' + column);

        cellElement.addClass(clazz);
        cellElement.attr('style', style);
    };

    presenter.addAdditionalStyles = function() {
        if (!presenter.model['Styles']) {
            return;
        }
        $.each(presenter.model['Styles'], function() {
            var columns = this['Column'],
                rows = this['Row'];

            if (rows) {
                rows = rows.split(',');
            }

            if (columns) {
                columns = columns.split(',');
            }

            for (var row = 0; row &lt; rows.length; row++) {
                for (var column = 0; column &lt; columns.length; column++) {
                    presenter.addAdditionalStyleToElement(rows[row], columns[column], this['Style'], this['Class']);
                }
            }
        });
    };

    presenter.setContainerWidth = function() {
        var viewWrapper = this.$view.find("#writing-calculations-wrapper");
        var width = $(viewWrapper).children().outerWidth();
        $(viewWrapper).css('width', width);
    };

    presenter.bindValueChangeEvent = function() {
        const $inputs = presenter.getInputs();
        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();

        $inputs.on('click', function(event) {
            event.stopPropagation();
        });

        $inputs.on('keyup', function(event) {
            presenter.onKeyUp(event);
        });

        $inputs.on('keypress', function(event) {
            presenter.onKeyPress(event);
        });

        $emptyBoxesInputs.on("change", function(event) {
            event.stopPropagation();

            var value = event.target.value;
            var rowIndex = $(event.target).attr("row");
            var cellIndex = $(event.target).attr("cell");
            var item = rowIndex + "-" + cellIndex;
            var isCorrect = 0;
            var answer = presenter.createAnswer(rowIndex, cellIndex, value);
            if(presenter.isCorrect(answer)) {
                isCorrect = 1;
            }

            if (presenter.isCommutativity &amp;&amp; presenter.isAllEmptyBoxInputsFilled()) {
                presenter.triggerValueChangeEvent("", "all", presenter.isAllEmptyBoxInputsCorrectlyFilled() ? 1 : 0);
            }

            if (!presenter.isCommutativity) {
                presenter.triggerValueChangeEvent(value, item, isCorrect);
            }

            if(!presenter.isCommutativity &amp;&amp; presenter.allAnswersCorrect()) {
                presenter.triggerValueChangeEvent("", "all", "");
            }
        });
    };

    presenter.onKeyUp = function(event) {
        event.stopPropagation();
        if (presenter.useNumericKeyboard) {
            var newText = String(event.target.value);
            var pattern = StringUtils.getNumericPattern();
            if (newText.length &gt; 0 &amp;&amp; !newText.match(pattern)) {
                var patternWithoutLastCharacter = pattern.slice(0, -1);
                var regExp = RegExp(patternWithoutLastCharacter);
                var match = regExp.exec(newText);
                var rowIndex = $(event.target).attr("row");
                var cellIndex = $(event.target).attr("cell");

                if (match) {
                    presenter.setCellElementValue(rowIndex, cellIndex, match[0]);
                } else {
                    presenter.setCellElementValue(rowIndex, cellIndex, "");
                }
            }
        }
    };

    presenter.onKeyPress = function(event) {
        event.stopPropagation();
        if (presenter.useNumericKeyboard) {
            var key = String.fromCharCode(!event.charCode ? event.which : event.charCode);
            var selectionStartIdx = event.target.selectionStart;
            var selectionEndIdx = event.target.selectionEnd;
            var oldText = String(event.target.value);
            var newText = oldText.substring(0, selectionStartIdx)
                            + key
                            + oldText.substring(selectionEndIdx);
            var pattern = StringUtils.getNumericPattern();
            if (!newText.match(pattern)) {
                event.preventDefault();
            }
        }
    };

    presenter.createView = function (elementsData) {
        presenter.elementsTable = [];
        presenter.gapElements = [];
        const $viewWrapper = this.$view.find("#writing-calculations-wrapper");
        elementsData.forEach((elementsRow, rowIndex) =&gt; {
            const createdElementsRow = [];
            const $rowWrapper = presenter.createRowWrapper(rowIndex);
            let cellIndex = 0;
            let gapInRowIndex = 0;
            elementsRow.forEach((elementData) =&gt; {
                const createdElement = presenter.createElement(elementData.type);

                if (elementData.type !== presenter.ELEMENT_TYPE.LINE) {
                    addCellClass(createdElement, cellIndex);
                }

                presenter.transformElement(createdElement, elementData.parsedValue, elementData.type);

                const ELEMENT_TYPES_WITH_POSITION = [
                    presenter.ELEMENT_TYPE.EMPTY_BOX,
                    presenter.ELEMENT_TYPE.NUMBER,
                    presenter.ELEMENT_TYPE.HELP_BOX
                ];
                if (elementData.isVisiblePosition()
                    &amp;&amp; (ELEMENT_TYPES_WITH_POSITION.includes(elementData.type))) {
                    presenter.addPosition(createdElement, elementData.getPosition());
                }

                $rowWrapper.append(createdElement);

                if (elementData.type !== presenter.ELEMENT_TYPE.DOT) {
                    cellIndex++;
                    createdElementsRow.push(createdElement);
                }

                if (elementData.type === presenter.ELEMENT_TYPE.EMPTY_BOX || elementData.type === presenter.ELEMENT_TYPE.HELP_BOX) {
                    presenter.gapElements.push({
                        x: createdElementsRow.length - 1,
                        y: presenter.tableElements.length,
                        el: createdElement,
                        gapInRowIndex: gapInRowIndex++
                    });
                }
            });
            $viewWrapper.append($rowWrapper);
            presenter.tableElements.push(createdElementsRow);
        });
    };

    function addCellClass(createdElement, cellIndex) {
        $(createdElement).addClass('cell-' + (cellIndex + 1));
    }

    presenter.addPosition = function(element, position) {
        const input = $(element).find(".writing-calculations-input, .container-number")[0];

        $(input).attr({
            "row" : position.rowIndex,
            "cell" : position.cellIndex
        });
    };

    presenter.setCellElementValue = function (row, cell, value) {
        const $inputs = presenter.getInputs();

        for (let i = 0; i &lt; $inputs.length; i++) {
            if ($($inputs[i]).attr("row") === row &amp;&amp; $($inputs[i]).attr("cell") === cell) {
                $($inputs[i]).val(value);
            }
        }
    };

    presenter.parseValue = function (value) {
        if (!this.isEmptyBox(value) &amp;&amp; !this.isHelpBox(value)) {
            return value;
        }

        const openingBracketIndex = 1;
        const endingBracketIndex = value.length - 1;
        return value.slice(openingBracketIndex, endingBracketIndex);
    };

    presenter.createRowWrapper = function(index) {
        const rowWrapper = document.createElement("div");
        rowWrapper.classList.add("wrapper-row");
        rowWrapper.classList.add("row-" + (index + 1));
        return $(rowWrapper);
    };

    presenter.createElement = function(type) {
        let createdElement;
        switch (type) {
            case this.ELEMENT_TYPE.NUMBER:
                createdElement = this.createWrapperAndContainer("number");
                break;
            case this.ELEMENT_TYPE.SYMBOL:
                createdElement = this.createWrapperAndContainer("symbol");
                break;
            case this.ELEMENT_TYPE.EMPTY_SPACE:
                createdElement = this.createWrapperAndContainer("emptySpace", 'wrapper-empty-space');
                break;
            case this.ELEMENT_TYPE.EMPTY_BOX:
                createdElement = this.createWrapperAndContainer("emptyBox");
                break;
            case this.ELEMENT_TYPE.LINE:
                createdElement = this.createWrapperAndContainer("line", 'wrapper-line');
                break;
            case this.ELEMENT_TYPE.DOT:
                createdElement = this.createWrapperAndContainer("dot", 'wrapper-dot');
                break;
            case this.ELEMENT_TYPE.HELP_BOX:
                createdElement = this.createWrapperAndContainer("helpBox");
                break;
        }

        return createdElement;
    };

    presenter.getContainerType = function ($el) {
        if ($el.hasClass("container-number")) return presenter.ELEMENT_TYPE.NUMBER;
        if ($el.hasClass("container-symbol")) return presenter.ELEMENT_TYPE.SYMBOL;
        if ($el.hasClass("container-emptySpace")) return presenter.ELEMENT_TYPE.EMPTY_SPACE;
        if ($el.hasClass("container-emptyBox")) return presenter.ELEMENT_TYPE.EMPTY_BOX;
        if ($el.hasClass("container-line")) return presenter.ELEMENT_TYPE.LINE;
        if ($el.hasClass("container-dot")) return presenter.ELEMENT_TYPE.DOT;
        if ($el.hasClass("container-helpBox")) return presenter.ELEMENT_TYPE.HELP_BOX;
        return -1;
    }

    presenter.createWrapperAndContainer = function(cssClass, wrapperClass) {
        if (!wrapperClass || wrapperClass === undefined) {
            wrapperClass = "wrapper-cell";
        }
        var wrapper = $("&lt;div&gt;&lt;/div&gt;");
        wrapper.addClass(wrapperClass);
        var container = $("&lt;div&gt;&lt;/div&gt;");
        container.addClass("container-" + cssClass);
        wrapper.append(container);
        return wrapper;
    };

    presenter.transformElement = function(element, value, type) {
        const $container = $(element).find("[class*=container]");
        let inputType, input;
        switch(type) {
            case this.ELEMENT_TYPE.EMPTY_SPACE:
                break;
            case this.ELEMENT_TYPE.EMPTY_BOX:
                inputType = presenter.useNumericKeyboard ? "tel" : "text";
                input = $("&lt;input type='" + inputType + "'&gt;");
                input.addClass("writing-calculations-input");
                if(!presenter.multisigns){
                    input.attr("maxlength", 1);
                }
                if (presenter.useNumericKeyboard) {
                    input.attr("step", "any");
                }
                $container.append(input);
                break;
            case this.ELEMENT_TYPE.LINE:
                break;
            case this.ELEMENT_TYPE.SYMBOL:
                $container.html(this.convertLaTeX(value));
                $container.data('value',value);
                break;
            case this.ELEMENT_TYPE.DOT:
                $container.html(value);
                break;
            case this.ELEMENT_TYPE.HELP_BOX:
                inputType = presenter.useNumericKeyboard ? "tel" : "text";
                input = $("&lt;input type='" + inputType + "'&gt;");
                input.addClass("writing-calculations-input");
                if (presenter.useNumericKeyboard) {
                    input.attr("step", "any");
                }
                input.val(value);
                $container.append(input);
                break;
            default:
                $container.html(value);
        }
    };

    presenter.convertLaTeX = function (value) {
        if (value === "*") {
            return presenter.signs['Multiplication'];
        } else if (value === ":" || value === ")") {
            return presenter.signs['Division'];
        } else if (value === "+") {
            return presenter.signs['Addition'];
        } else if (value === "-") {
            return presenter.signs['Subtraction'];
        } else if (value === "#") {
            return presenter.signs['Equals'];
        }
    };

    presenter.getElementType = function(element) {
        if( this.isInteger(element) ) return this.ELEMENT_TYPE.NUMBER;
        if( this.isSymbol(element) ) return this.ELEMENT_TYPE.SYMBOL;
        if( this.isEmptySpace(element) ) return this.ELEMENT_TYPE.EMPTY_SPACE;
        if( this.isEmptyBox(element) ) return this.ELEMENT_TYPE.EMPTY_BOX;
        if( this.isLine(element) ) return this.ELEMENT_TYPE.LINE;
        if( this.isDot(element)) return this.ELEMENT_TYPE.DOT;
        if( this.isHelpBox(element)) return this.ELEMENT_TYPE.HELP_BOX;
    };

    presenter.isDot = function(element) {
        return element == "." || element == ",";
    };

    /**
     Check if valid empty box == will be empty box in run
     @method isEmptyBox

     @param {String} element element HTML
    */
    presenter.isEmptyBox = function(element) {
        if ((element.length &lt; 2)
            || (element[0] !== '[' || element[element.length - 1] !== ']')) {
            return false;
        }

        if (!presenter.multisigns) {
            const pattern = /\[[\d.,]?\]/g; // matches: '[number]' or '[.]' or '[,]'
            return pattern.test(element);
        }

        const content = element.slice(1, element.length - 1);
        return (presenter.isDot(content) || presenter.isIntegerOrFloat(content));
    };

    /**
     Check if valid help box
     @method isHelpBox

     @param {String} element element HTML
    */
    presenter.isHelpBox = function(element) {
        if ((element.length &lt; 2)
            || (element[0] !== '{' || element[element.length - 1] !== '}')) {
            return false;
        }

        const content = element.slice(1, element.length - 1);
        return (presenter.isDot(content) || presenter.isIntegerOrFloat(content)) || content === "";
    };

    presenter.isEmptySpace = function(element) {
        return element == "_";
    };

    presenter.isSymbol = function(element) {
        if (element.length != 1) {
            return false;
        }
        const pattern = /[#+\-*:\)]/g; // matches: '#', '+', '-', ':', ')' and '*'
        return pattern.test(element);
    };

    presenter.isInteger = function (value) {
        if (value === undefined || value === null) {
            return false;
        }
        value = value.toString().trim();
        const commonTestsResult = isNaNCommonTests(value);
        if (commonTestsResult === false) {
            return false;
        }

        if (value % 1 !== 0) {
            return false;
        }

        return /\d+/.test(value);
    };

    presenter.isIntegerOrFloat = function (value) {
        if (value === undefined || value === null) {
            return false;
        }
        value = value.toString().trim();
        const commonTestsResult = isNaNCommonTests(value);
        if (commonTestsResult === false) {
            return false;
        }

        if (value.charAt(0) == "-") {
            value = value.slice(1, value.length);
        }

        let i, commasNumber = 0;
        const digitPattern = /[0-9]/;
        const separatorPattern = /[,.]/;

        if (separatorPattern.test(value.charAt(0))) {
            return false;
        }

        for (i = 0; i &lt; value.length; i++) {
            if (separatorPattern.test(value.charAt(i))) {
                if (commasNumber &gt;= 1) {
                    return false;
                }
                commasNumber ++;
            } else {
                if (digitPattern.test(value.charAt(i))) {
                    continue;
                } else {
                    return false;
                }
            }
        }

        return true;
    };

    function isNaNCommonTests (value) {
        value = value.trim();
        return !(ModelValidationUtils.isStringEmpty(value)
            || isStartsWithPlus(value)
            || isTooManyZeroes(value)
            || isMinusZero(value)
            || isOnlyMinus(value)
        );
    }

    function isStartsWithPlus (value) {
        return value.charAt(0) == "+";
    }

    function isMinusZero (value) {
        return value == "-0";
    }

    function isOnlyMinus (value) {
        return value == "-";
    }

    function isTooManyZeroes (value) {
        const tooManyZeroesPattern = /^-?0{2,}/;
        return tooManyZeroesPattern.test(value);
    }

    presenter.isLine = function(element) {
        return element == "=";
    };

    presenter.convertStringToArray = function(stringToConvert) {
        return stringToConvert.split(/[\n\r]+/);
    };

    presenter.isCorrect = function(answer) {
        var result = false;
        var correctAnswers = presenter.correctAnswersList;
        for (var i = 0; i &lt; correctAnswers.length; i++) {
            if (presenter.isEqual(answer, correctAnswers[i])) {
                result = true;
            }
        }
        return result;
    };

    presenter.allAnswersCorrect = function() {
        var maxScore = presenter.getPoints("all");
        var score = presenter.getPoints("correct");
        var errorCount = presenter.getPoints("incorrect");

        return maxScore === score &amp;&amp; errorCount === 0;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'isAllOK': presenter.isAllOK,
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.isAllOK = function () {
        var maxScore = presenter.getPoints("all"),
            score = presenter.getPoints("correct"),
            errorCount = presenter.getPoints("incorrect");

        return maxScore === score &amp;&amp; errorCount === 0;
    };

    presenter.isEqual = function(answer, correctAnswer) {
        return answer.value === correctAnswer.value &amp;&amp; answer.rowIndex === correctAnswer.rowIndex &amp;&amp; answer.cellIndex === correctAnswer.cellIndex;
    };

    presenter.getInputs = function() {
        return this.$view.find(".writing-calculations-input");
    }

    presenter.getEmptyBoxesInputs = function() {
        return this.$view.find(".container-emptyBox .writing-calculations-input");
    };

    presenter.getHelpBoxesInputs = function() {
        return this.$view.find(".container-helpBox .writing-calculations-input");
    };

    presenter.isAllEmptyBoxInputsFilled = function() {
        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();

        for (let i = 0; i &lt; $emptyBoxesInputs.length; i++) {
            if ($($emptyBoxesInputs[i]).val().length == 0) return false;
        }

        return true;
    };

    presenter.handleShownAnswers = function () {
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
    }

    presenter.setShowErrorsMode = function() {
        if(presenter.isNotActivity){
            return;
        }

        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();
        const $helpBoxesInputs = presenter.getHelpBoxesInputs();
        disableInputs($helpBoxesInputs);

        presenter.handleShownAnswers();

        if (!presenter.isCommutativity) {
            $.each($emptyBoxesInputs, function(){
                const answer = presenter.createAnswer($(this).attr("row"), $(this).attr("cell"), $(this).val());

                if (ModelValidationUtils.isStringEmpty($(this).val())) {
                    presenter.markEmpty($(this));
                } else if (presenter.isCorrect(answer)) {
                    presenter.markCorrect($(this));
                } else {
                    presenter.markIncorrect($(this));
                }
            });
        } else if (presenter.isAllEmptyBoxInputsFilled()) {
            const isCorrect = presenter.isAllEmptyBoxInputsCorrectlyFilled();

            if (isCorrect) {
                presenter.$view.addClass('correct');
            } else {
                presenter.$view.addClass('wrong');
            }

            disableInputs($emptyBoxesInputs);
        } else {
            disableInputs($emptyBoxesInputs);
        }
    };

    function disableInputs(inputs) {
        inputs.attr("disabled", "disabled");
    }

    presenter.compareAnswers = function(correctAnswers, userAnswers) {
        var answers = $.extend(true, [], userAnswers);

        correctAnswers = $.extend(true, [], correctAnswers);

        var userResult = answers.pop(),
            declaredResult = correctAnswers.pop();

        var cleanedAnswers = [],
            cleanedCorrectAnswers = [],
            logicalFoundArray = [],
            found = false,
            result = true,
            i;

        for (i = 0; i &lt;= answers.length; i++) {
            if (answers[i] !== undefined) {
                cleanedAnswers.push(answers[i]);
            }

            if (correctAnswers[i] !== undefined) {
                cleanedCorrectAnswers.push(correctAnswers[i]);
            }
        }

        for (i = 0; i &lt; cleanedCorrectAnswers.length; i++) {
            found = presenter.wasRowFound(cleanedCorrectAnswers[i], cleanedAnswers);
            logicalFoundArray.push(found);
        }

        jQuery.each(logicalFoundArray, function (_, element) {
            if (!element) {
                result = false;
                return false; // jQuery.each break statement
            }
        });

        result = result &amp;&amp; presenter.compareResults(userResult, declaredResult);

        return result;
    };


    presenter.compareResults = function(userResult, declaredResult) {
        return userResult.toString() === declaredResult.toString();
    };

    /*
     This method get one of correctAnswers row
     and comparing it with each givenAnswers rows.

     Takes: correctAnswers as all correct values in one row
     and givenAnswers as answers in all rows given by user.

     Returns: boolean value if declared row in correctAnswers
     was found in givenAnswers.
     */
    presenter.wasRowFound = function(correctAnswers, givenAnswers) {
        var wasRowFound = false;

        for (var j = 0; j &lt; givenAnswers.length; j++) { //answers given by user
            if (givenAnswers[j] !== undefined &amp;&amp; correctAnswers.toString() === givenAnswers[j].toString()) {
                delete givenAnswers[j];
                wasRowFound = true;
                break;
            } else {
                wasRowFound = false;
            }
        }

        return wasRowFound;
    };

    presenter.getAllAnswers = function($elements) {
        const answers = [];

        $.each($elements, function(){
            if ($(this).hasClass('writing-calculations-input')) {
                if (answers[$(this).attr("row") - 1] === undefined) {
                    answers[$(this).attr("row") - 1] = [$(this).val()];
                } else {
                    answers[$(this).attr("row") - 1].push($(this).val());
                }
            } else if ($(this).hasClass('container-number')) {
                if (answers[$(this).attr("row") - 1] === undefined) {
                    answers[$(this).attr("row") - 1] = [$(this).html()];
                } else {
                    answers[$(this).attr("row") - 1].push($(this).html());
                }
            }
        });

        return answers;
    };

    presenter.isAllEmptyBoxInputsCorrectlyFilled = function() {
        const $elements = this.$view.find('.container-number, .container-emptyBox .writing-calculations-input');
        const answers = presenter.getAllAnswers($elements);

        return presenter.compareAnswers(presenter.answers, answers);
    };

    presenter.createAnswer = function(rowIndex, cellIndex, rawValue) {
        return {
            rowIndex: parseInt(rowIndex, 10),
            cellIndex: parseInt(cellIndex, 10),
            value: presenter.parseValue(rawValue)
        }
    };

    presenter.createElementData = function (rowIndex, cellIndex, elementValue, isVisiblePosition = false) {
        return new ElementData(rowIndex, cellIndex, elementValue, isVisiblePosition)
    };

    function ElementData (rowIndex, cellIndex, elementValue, isVisiblePosition = false) {
        this.rawValue = elementValue;
        this.parsedValue = presenter.parseValue(elementValue);
        this.type = presenter.getElementType(elementValue);

        // Element's position data
        this.visiblePosition = isVisiblePosition;
        this.rowIndex = rowIndex;
        this.cellIndex = cellIndex;
    }

    ElementData.prototype.getPosition = function () {
        return {
            rowIndex: this.rowIndex,
            cellIndex: this.cellIndex,
        };
    }

    ElementData.prototype.createAnswer = function () {
        return {
            rowIndex: this.rowIndex,
            cellIndex: this.cellIndex,
            value: this.parsedValue,
        };
    }

    ElementData.prototype.isVisiblePosition = function () {
        return this.visiblePosition;
    }

    presenter.markIncorrect = function(element) {
        presenter.markAs(element, "incorrect");
    };

    presenter.markCorrect = function(element) {
        presenter.markAs(element, "correct");
    };

    presenter.markEmpty = function(element) {
        presenter.markAs(element, "empty");
    };

    presenter.markAs = function(element, className) {
        $(element).addClass(className);
        $(element).attr("disabled", "disabled");
    };

    presenter.setWorkMode = function() {
        if(presenter.isNotActivity){
            return;
        }

        this.clean(true, false);
    };

    presenter.reset = function() {
        presenter.isErrorCheckingMode &amp;&amp; presenter.setWorkMode();
        presenter.isShowAnswersActive &amp;&amp; presenter.hideAnswers();
        presenter.isGradualShowAnswersActive &amp;&amp; presenter.gradualHideAnswers();
        presenter.clean(true, true);

        const $helpBoxesInputs = presenter.getHelpBoxesInputs();
        const defaultValues = presenter.helpBoxesDefaultValues.flat();
        $.each($helpBoxesInputs, function(index){
            $(this).val(defaultValues[index]);
        });
        presenter.userAnswers = [];
        presenter.helpBoxesUserAnswers = [];

        presenter.setVisibility(presenter.isVisibleByDefault);
        presenter.isVisible = presenter.isVisibleByDefault;
        presenter.isScoreSaved = false;
        presenter.isDisabled = false;
    };

    presenter.clean = function(removeMarks, removeValues) {
        const $inputs = presenter.getInputs();
        $.each($inputs, function(){
            if(removeMarks) {
                presenter.removeMark($(this));
            }
            if(removeValues) {
                presenter.removeValue($(this));
            }
        });
        if (presenter.$view.hasClass('wrong')) {
            presenter.$view.removeClass('wrong');
        } else if (presenter.$view.hasClass('correct')) {
            presenter.$view.removeClass('correct');
        }
    };

    presenter.removeMark = function(element) {
        $(element).removeClass("incorrect correct empty");
        $(element).removeAttr("disabled");
    };

    presenter.removeValue = function(element) {
        $(element).val("");
    };

    presenter.getEmptyBoxesInputsData = function() {
        const $inputs = presenter.getEmptyBoxesInputs();
        const inputsData = {
            values : [],
            correctAnswersCount : 0,
            incorrectAnswersCount : 0
        };

        presenter.handleShownAnswers();

        $.each($inputs, function () {
            let value = $(this).val();
            value = presenter.parseValue(value);
            inputsData.values.push(value);

            if (value === undefined || value === "") return true; // jQuery.each continue

            const answer = presenter.createAnswer($(this).attr("row"), $(this).attr("cell"), $(this).val());
            if( presenter.isCorrect(answer) ) {
                inputsData.correctAnswersCount++;
            } else {
                inputsData.incorrectAnswersCount++;
            }
        });
        return inputsData;
    };

    presenter.getHelpBoxesInputsData = function() {
        const $inputs = presenter.getHelpBoxesInputs();
        const inputsData = {
            values : [],
        };

        presenter.handleShownAnswers();

        $.each($inputs, function () {
            let value = $(this).val();
            value = presenter.parseValue(value);
            inputsData.values.push(value);
        });
        return inputsData;
    };

    presenter.getState = function() {
        presenter.handleShownAnswers();

        return JSON.stringify({
            "inputsData" : this.getEmptyBoxesInputsData(),
            "helpBoxesInputsData" : presenter.getHelpBoxesInputsData(),
            "isVisible" : presenter.isVisible,
        });
    };

    presenter.setState = function(stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) return;

        const state = JSON.parse(stateString);
        if (state.inputsData) {
            const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();
            const inputsData = state.inputsData;
            $.each($emptyBoxesInputs, function(index){
                $(this).val(inputsData.values[index].toString());
            });
        }

        if (state.helpBoxesInputsData) {
            const $helpBoxesInputs = presenter.getHelpBoxesInputs();
            const helpBoxesInputsData = state.helpBoxesInputsData;
            $.each($helpBoxesInputs, function(index){
                $(this).val(helpBoxesInputsData.values[index].toString());
            });
        }

        if (state.isVisible != undefined) {
            presenter.isVisible = state.isVisible;
            presenter.setVisibility(presenter.isVisible);
        }
    };

    presenter.getScore = function() {
        if(presenter.isNotActivity){
            return 0;
        }

        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return presenter.currentScore;
        }
        return this.getPoints("correct");
    };

    presenter.getMaxScore = function() {
        if(presenter.isNotActivity){
            return 0;
        }

        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return presenter.currentMaxScore;
        }
        return this.getPoints("all");
    };

    presenter.getErrorCount = function() {
        if(presenter.isNotActivity){
            return 0;
        }

        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            return presenter.currentErrorCount;
        }
        return this.getPoints("incorrect");
    };

    presenter.getPoints = function(type) {
        const inputsData = this.getEmptyBoxesInputsData();

        if (presenter.isCommutativity) {
            switch (type) {
                case 'correct':
                    return presenter.isAllEmptyBoxInputsCorrectlyFilled() ? 1 : 0;
                case 'incorrect':
                    if (presenter.isAllEmptyBoxInputsFilled()) {
                        return presenter.isAllEmptyBoxInputsCorrectlyFilled() ? 0 : 1;
                    } else {
                        return 0;
                    }
                case 'all':
                    return 1;
                default:
                    return 0;
            }
        } else {
            switch (type) {
                case 'correct':
                    return inputsData.correctAnswersCount;
                case 'incorrect':
                    return inputsData.incorrectAnswersCount;
                case 'all':
                    return inputsData.values.length;
                default:
                    return 0;
            }
        }
    };

    presenter.createEventData = function(value, item, isCorrect) {
        return {
            source : this.model.ID,
            item : "" + item,
            value : "" + value,
            score : "" + isCorrect
        };
    };

    presenter.triggerValueChangeEvent = function(value, item, isCorrect) {
        var eventData = this.createEventData(value, item, isCorrect);
        if (this.playerController !== null) {
            this.playerController.getEventBus().sendEvent('ValueChanged', eventData);
        }
    };

    presenter.setPlayerController = function(controller) {
        this.playerController = controller;
    };

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "GradualShowAnswers":
                presenter.gradualShowAnswers(eventData);
                break;

            case "HideAnswers":
                presenter.hideAnswers();
                break;

            case "GradualHideAnswers":
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.disableInputs = function () {
        if (presenter.isDisabled) {
            return;
        }

        presenter.isDisabled = true;
        const $inputs = presenter.getInputs();
        $.each($inputs, function() {
            $(this).attr("disabled", true);
        });
    };

    presenter.enableInputs = function () {
        presenter.isDisabled = false;

        const $inputs = presenter.getInputs();
        $.each($inputs, function() {
            $(this).attr("disabled", false);
        });
    };

    presenter.showAnswers = function () {
        if (presenter.isNotActivity){
            return;
        }

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.saveCurrentScore();
        presenter.isShowAnswersActive = true;
        presenter.clean(true,false);
        presenter.isDisabled = true;
        const correctAnswers = presenter.correctAnswersList;

        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();
        $.each($emptyBoxesInputs, function(index){
            $(this).addClass('writing-calculations_show-answers');
            $(this).attr("disabled", true);
            presenter.userAnswers.push($(this).val());
            $(this).val(correctAnswers[index].value);
        });

        const $helpBoxesInputs = presenter.getHelpBoxesInputs();
        $.each($helpBoxesInputs, function(){
            $(this).addClass('writing-calculations_show-answers');
            $(this).attr("disabled", true);
            presenter.helpBoxesUserAnswers.push($(this).val());
            $(this).val("");
        });
    };

    presenter.gradualShowAnswers = function (eventData) {
        presenter.disableInputs();

        if (eventData.moduleID !== presenter.ID) {
            return;
        }

        if (presenter.showAllAnswersInGSA) {
            presenter.showAnswers();
            return;
        }

        if (!presenter.isGradualShowAnswersActive) {
            const $helpBoxesInputs = presenter.getHelpBoxesInputs();
            $.each($helpBoxesInputs, function(){
                $(this).addClass('writing-calculations_show-answers');
                $(this).attr("disabled", true);
                presenter.helpBoxesUserAnswers.push($(this).val());
                $(this).val("");
            });
        }
        presenter.isGradualShowAnswersActive = true;
        presenter.saveCurrentScore();
        const input = presenter.getEmptyBoxesInputs()[eventData.item];

        $(input).addClass('writing-calculations_show-answers');
        presenter.userAnswers.push($(input).val());
        $(input).val(presenter.correctAnswersList[eventData.item].value);
    };

    presenter.hideAnswers = function () {
        if (presenter.isNotActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.isShowAnswersActive = false;
        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();
        $.each($emptyBoxesInputs, function(index){
            $(this).val(presenter.userAnswers[index]);
            $(this).removeClass('writing-calculations_show-answers');
            $(this).attr("disabled", false);
        });

        const $helpBoxesInputs = presenter.getHelpBoxesInputs();
        $.each($helpBoxesInputs, function(index){
            $(this).val(presenter.helpBoxesUserAnswers[index]);
            $(this).removeClass('writing-calculations_show-answers');
            $(this).attr("disabled", false);
        });

        presenter.userAnswers = [];
        presenter.helpBoxesUserAnswers = [];
        presenter.isDisabled = false;
    };

    presenter.gradualHideAnswers = function () {
        if (presenter.showAllAnswersInGSA &amp;&amp; presenter.isShowAnswersActive) {
            presenter.hideAnswers();
            return;
        }

        presenter.enableInputs();

        if (presenter.isNotActivity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.isGradualShowAnswersActive = false;
        presenter.isScoreSaved = false;

        const $emptyBoxesInputs = presenter.getEmptyBoxesInputs();
        presenter.userAnswers.forEach((userAnswer, index) =&gt; {
            const input = $emptyBoxesInputs[index];
            $(input).val(presenter.userAnswers[index]);
            $(input).removeClass('writing-calculations_show-answers');
        });

        const $helpBoxesInputs = presenter.getHelpBoxesInputs();
        $.each($helpBoxesInputs, function(index){
            $(this).val(presenter.helpBoxesUserAnswers[index]);
            $(this).removeClass('writing-calculations_show-answers');
        });

        presenter.userAnswers = [];
        presenter.helpBoxesUserAnswers = [];
    };

    presenter.saveCurrentScore = function () {
        if (presenter.isScoreSaved) {
            return;
        }
        presenter.currentScore = presenter.getScore();
        presenter.currentErrorCount = presenter.getErrorCount();
        presenter.currentMaxScore = presenter.getMaxScore();

        presenter.isScoreSaved = true;
    }

    presenter.getActivitiesCount = function () {
        return presenter.showAllAnswersInGSA ? 1 : presenter.getEmptyBoxesInputs().length;
    }

    function createRowsStatuesOfNavigability(modelRowsAltTexts, elementsData) {
        presenter.rowsStatusesOfNavigabilityInAltTTSNavigation = [{rowIndex: -1, isNavigable: true}]; // First navigable row is DESCRIPTION_OF_OPERATION
        elementsData.forEach((rowElements, rowIndex) =&gt; {
            let isNavigable = false;
            for (let columnIndex = 0; columnIndex &lt; rowElements.length; columnIndex++) {
                const containerType = rowElements[columnIndex].type;
                if (containerType !== presenter.ELEMENT_TYPE.LINE &amp;&amp; containerType !== presenter.ELEMENT_TYPE.EMPTY_SPACE) {
                    isNavigable = true;
                    break;
                }
            }
            presenter.rowsStatusesOfNavigabilityInAltTTSNavigation.push({rowIndex: rowIndex, isNavigable: isNavigable});
        });
    }

    presenter.validateRowsAltTexts = function(modelRowsAltTexts) {
        const numberOfNavigableRows = presenter.rowsStatusesOfNavigabilityInAltTTSNavigation.filter((status) =&gt; status.isNavigable).length;
        const numberOfAdditionalRows = 1;
        if (numberOfNavigableRows - numberOfAdditionalRows === modelRowsAltTexts.length) {
            return getCorrectObject(modelRowsAltTexts);
        }
        return getErrorObject("RAT01", {
            rowsAltTextsNumber: modelRowsAltTexts.length,
            navigableRowsNumber: numberOfNavigableRows - numberOfAdditionalRows
        });
    }

    presenter.validateModelValue = function (modelValue) {
        const rows = presenter.convertStringToArray(modelValue);
        const elementsData = [];

        for (let rowIndex = 0; rowIndex &lt; rows.length; rowIndex++) {
            let row = rows[rowIndex];

            const validatedAnswerBracketsNumber = validateAnswerBracketsNumber(row, rowIndex);
            if (!validatedAnswerBracketsNumber.isValid) {
                return validatedAnswerBracketsNumber;
            }

            const validatedHelpBracketsNumber = validateHelpBracketsNumber(row, rowIndex);
            if (!validatedHelpBracketsNumber.isValid) {
                return validatedHelpBracketsNumber;
            }

            let cellBoxIndex = 0;
            elementsData.push([]);
            for (let startingIndex = 0; startingIndex &lt; row.length; ) {
                let elementValue = row[startingIndex];
                let elementLength = 1;
                let elementData = {};

                const isEndOfAnswerGap = elementValue === ']';
                if (isEndOfAnswerGap) {
                    return getErrorObject("V02", {rowIndex: rowIndex + 1});
                }

                const isEndOfHelpGap = elementValue === '}';
                if (isEndOfHelpGap) {
                    return getErrorObject("V07", {rowIndex: rowIndex + 1});
                }

                const isStartOfAnswerGap = elementValue === '[';
                const isStartOfHelpGap = elementValue === '{';
                if (isStartOfHelpGap) {
                    const validatedGapLength = validateHelpGapLength(row, rowIndex, startingIndex);
                    if (!validatedGapLength.isValid) {
                        return validatedGapLength;
                    }

                    elementLength = validatedGapLength.value;
                    elementValue = row.slice(startingIndex, startingIndex + elementLength);
                    elementData = presenter.createElementData(rowIndex + 1, ++cellBoxIndex, elementValue, true);
                    if (elementData.type !== presenter.ELEMENT_TYPE.HELP_BOX) {
                        return getErrorObject("V08", {rowIndex: rowIndex + 1, value: elementData.rawValue});
                    }

                    if (presenter.helpBoxesDefaultValues[rowIndex] === undefined) {
                        presenter.helpBoxesDefaultValues[rowIndex] = [];
                    }

                    const answer = elementData.createAnswer();
                    presenter.helpBoxesDefaultValues[rowIndex].push(answer.value);
                } else if (isStartOfAnswerGap) {
                    const validatedGapLength = validateAnswerGapLength(row, rowIndex, startingIndex);
                    if (!validatedGapLength.isValid) {
                        return validatedGapLength;
                    }

                    elementLength = validatedGapLength.value;
                    elementValue = row.slice(startingIndex, startingIndex + elementLength);
                    elementData = presenter.createElementData(rowIndex + 1, ++cellBoxIndex, elementValue, true);
                    if (elementData.type !== presenter.ELEMENT_TYPE.EMPTY_BOX) {
                        return getErrorObject("V05", {rowIndex: rowIndex + 1, value: elementData.rawValue});
                    }

                    if (presenter.answers[rowIndex] === undefined) {
                        presenter.answers[rowIndex] = [];
                    }

                    const answer = elementData.createAnswer();
                    presenter.answers[rowIndex].push(answer.value);
                    presenter.correctAnswersList.push(answer);
                } else {
                    if (presenter.isCommutativity) {
                        ++cellBoxIndex;
                    }
                    elementData = presenter.createElementData(rowIndex + 1, cellBoxIndex, elementValue, presenter.isCommutativity);
                    if (!isNaN(parseInt(elementValue, 10))) {
                        if (presenter.answers[rowIndex] === undefined) {
                            presenter.answers[rowIndex] = [];
                        }

                        presenter.answers[rowIndex].push(elementValue);
                    }
                }
                startingIndex += elementLength;
                elementsData[rowIndex].push(elementData);
            }
        }
        return getCorrectObject(elementsData);
    }

    function validateAnswerBracketsNumber(row, rowIndex) {
        const errorMessageSubstitutions = {rowIndex: rowIndex + 1};
        const openingBracketsCount = row.split('').filter(x =&gt; x === '[').length;
        const closingBracketsCount = row.split('').filter(x =&gt; x === ']').length;
        if (openingBracketsCount &gt; closingBracketsCount) {
            return getErrorObject("V01", errorMessageSubstitutions);
        } else if (openingBracketsCount &lt; closingBracketsCount) {
            return getErrorObject("V02", errorMessageSubstitutions);
        }
        return getCorrectObject(openingBracketsCount + closingBracketsCount);
    }

    function validateHelpBracketsNumber(row, rowIndex) {
        const errorMessageSubstitutions = {rowIndex: rowIndex + 1};
        const openingBracketsCount = row.split('').filter(x =&gt; x === '{').length;
        const closingBracketsCount = row.split('').filter(x =&gt; x === '}').length;
        if (openingBracketsCount &gt; closingBracketsCount) {
            return getErrorObject("V06", errorMessageSubstitutions);
        } else if (openingBracketsCount &lt; closingBracketsCount) {
            return getErrorObject("V07", errorMessageSubstitutions);
        }
        return getCorrectObject(openingBracketsCount + closingBracketsCount);
    }

    function validateAnswerGapLength(rowString, rowIndex, startIndex) {
        const errorMessageSubstitutions = {rowIndex: rowIndex + 1};

        const nextClosingAnswerBracketIndex = rowString.indexOf(']', startIndex);
        if (nextClosingAnswerBracketIndex === -1) {
            return getErrorObject("V01", errorMessageSubstitutions);
        }

        const nextOpeningAnswerBracketIndex = rowString.indexOf('[', startIndex + 1);
        if (nextOpeningAnswerBracketIndex !== - 1 &amp;&amp; nextOpeningAnswerBracketIndex &lt;= nextClosingAnswerBracketIndex) {
            return getErrorObject("V01", errorMessageSubstitutions);
        }

        const length = nextClosingAnswerBracketIndex - startIndex + 1;
        if (length === 2) {
            return getErrorObject("V03", errorMessageSubstitutions);
        }

        if (!presenter.multisigns &amp;&amp; length !== 3) {
            return getErrorObject("V04", errorMessageSubstitutions);
        }
        return getCorrectObject(length);
    }

    function validateHelpGapLength(rowString, rowIndex, startIndex) {
        const errorMessageSubstitutions = {rowIndex: rowIndex + 1};

        const nextClosingHelpBracketIndex = rowString.indexOf('}', startIndex);
        if (nextClosingHelpBracketIndex === -1) {
            return getErrorObject("V06", errorMessageSubstitutions);
        }

        const nextOpeningHelpBracketIndex = rowString.indexOf('{', startIndex + 1);
        if (nextOpeningHelpBracketIndex !== - 1 &amp;&amp; nextOpeningHelpBracketIndex &lt;= nextClosingHelpBracketIndex) {
            return getErrorObject("V07", errorMessageSubstitutions);
        }

        const length = nextClosingHelpBracketIndex - startIndex + 1;
        return getCorrectObject(length);
    }

    presenter.showErrorMessage = function(message, substitutions) {
        let errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            let messageSubst = message;
            for (const key in substitutions) {
                if (!substitutions.hasOwnProperty(key)) continue;
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.$view.html(errorContainer);
    };

    function getErrorObject (errorCode, errorMessageSubstitutions) {
        return {
            isValid: false,
            errorCode: errorCode,
            errorMessageSubstitutions: errorMessageSubstitutions,
        };
    }

    function getCorrectObject (value) {
        return {
            isValid: true,
            value: value
        };
    }

    presenter.setWCAGStatus = function(isOn) {
        presenter.isWCAGOn = isOn;
    }

     presenter.keyboardController = function(keycode, isShift, event) {
        if (!presenter.isGapFocused ||
            (isAlternativeKeyboardNavigationWithTTS()
                &amp;&amp; [window.KeyboardControllerKeys.ARROW_LEFT, window.KeyboardControllerKeys.ARROW_RIGHT].includes(keycode))
        ) {
            event.preventDefault();
        }
        const shouldReadAfterMove = !presenter.isGapFocused || isAlternativeKeyboardNavigationWithTTS();

        switch (keycode) {
            case window.KeyboardControllerKeys.TAB:
                if (isShift) {
                    prevTabElement();
                    if (shouldReadAfterMove) readSelectedElement();
                } else {
                    nextTabElement()
                    if (shouldReadAfterMove) readSelectedElement();
                }
                break;
            case window.KeyboardControllerKeys.ENTER:
                if (isShift) {
                    exitModuleInTTS();
                } else {
                    enter();
                    readSelectedElement();
                }
                break;
            case window.KeyboardControllerKeys.SPACE:
                break;
            case window.KeyboardControllerKeys.ARROW_UP:
                prevRow();
                if (shouldReadAfterMove) readSelectedElement();
                break;
            case window.KeyboardControllerKeys.ARROW_DOWN:
                nextRow();
                if (shouldReadAfterMove) readSelectedElement();
                break;
            case window.KeyboardControllerKeys.ARROW_LEFT:
                prevColumn();
                if (shouldReadAfterMove) readSelectedElement();
                break;
            case window.KeyboardControllerKeys.ARROW_RIGHT:
                nextColumn();
                if (shouldReadAfterMove) readSelectedElement();
                break;
            case window.KeyboardControllerKeys.ESCAPE:
                escape(event);
                break;
        }
    }

    function nextRow() {
        if (!presenter.isWCAGOn || (presenter.isGapFocused &amp;&amp; !presenter.useAlternativeTTSNavigation)) return;
        if (presenter.keyboardState.y + 1 &lt; presenter.tableElements.length) {
            if (presenter.useAlternativeTTSNavigation) {
                presenter.keyboardState.y = findNextNavigableRowIndexForAlternativeTTSNavigation(presenter.keyboardState.y);
            } else {
                presenter.keyboardState.y += 1;
            }
            if (presenter.keyboardState.y !== DESCRIPTION_OF_OPERATION_ROW_INDEX) {
                if (presenter.useAlternativeTTSNavigation) {
                    presenter.keyboardState.x = ALT_TEXT_FOR_ROW_COLUMN_INDEX;
                } else if (presenter.keyboardState.x &gt;= presenter.tableElements[presenter.keyboardState.y].length) {
                    presenter.keyboardState.x = presenter.tableElements[presenter.keyboardState.y].length - 1;
                }
            }
        }
        updateGapIndex();
        selectKeyboardNavigationElement();
    }

    function prevRow() {
        if (!presenter.isWCAGOn || (presenter.isGapFocused &amp;&amp; !presenter.useAlternativeTTSNavigation)) return;
        if (presenter.keyboardState.y - 1 &gt;= DESCRIPTION_OF_OPERATION_ROW_INDEX) {
            if (presenter.useAlternativeTTSNavigation) {
                presenter.keyboardState.y = findPrevNavigableRowIndexForAlternativeTTSNavigation(presenter.keyboardState.y);
            } else {
                presenter.keyboardState.y -= 1;
            }
            if (presenter.keyboardState.y === DESCRIPTION_OF_OPERATION_ROW_INDEX) {
                presenter.keyboardState.x = DESCRIPTION_OF_OPERATION_COLUMN_INDEX;
            } else if (presenter.useAlternativeTTSNavigation) {
                presenter.keyboardState.x = ALT_TEXT_FOR_ROW_COLUMN_INDEX;
            } else if (presenter.keyboardState.x &gt;= presenter.tableElements[presenter.keyboardState.y].length) {
                presenter.keyboardState.x = presenter.tableElements[presenter.keyboardState.y].length - 1;
            }
        }
        updateGapIndex();
        selectKeyboardNavigationElement();
    }

    function nextColumn() {
        if (!presenter.isWCAGOn || (presenter.isGapFocused &amp;&amp; !presenter.useAlternativeTTSNavigation)) return;
        if (presenter.keyboardState.y !== DESCRIPTION_OF_OPERATION_ROW_INDEX) {
            if (presenter.useAlternativeTTSNavigation) {
                presenter.keyboardState.x = findNextGapColumnInRow(presenter.keyboardState.y, presenter.keyboardState.x);
            } else if (presenter.keyboardState.x + 1 &lt; presenter.tableElements[presenter.keyboardState.y].length) {
                presenter.keyboardState.x += 1;
            }
        }
        updateGapIndex();
        selectKeyboardNavigationElement();
    }

    function findNextGapColumnInRow(y, x) {
        for (let gapIndex = 0; gapIndex &lt; presenter.gapElements.length; gapIndex++) {
            const gapElement = presenter.gapElements[gapIndex];
            if (gapElement.y !== y) {
                continue;
            }
            if (gapElement.x &gt; x) {
                return gapElement.x
            }
        }
        return x;
    }

    function prevColumn() {
        if (!presenter.isWCAGOn || (presenter.isGapFocused &amp;&amp; !presenter.useAlternativeTTSNavigation)) return;
        if (presenter.keyboardState.y !== DESCRIPTION_OF_OPERATION_ROW_INDEX) {
            if (presenter.useAlternativeTTSNavigation) {
                presenter.keyboardState.x = findPrevColumnInRow(presenter.keyboardState.y, presenter.keyboardState.x);
            } else if (presenter.keyboardState.x &gt; 0) {
                presenter.keyboardState.x -= 1;
            }
        }
        updateGapIndex();
        selectKeyboardNavigationElement();
    }

    function findPrevColumnInRow(y, x) {
        for (let gapIndex = presenter.gapElements.length - 1; gapIndex &gt;= 0; gapIndex--) {
            const gapElement = presenter.gapElements[gapIndex];
            if (gapElement.y !== y) {
                continue;
            }
            if (x === undefined) {
                return gapElement.x
            } else if (gapElement.x &lt; x) {
                return gapElement.x
            }
        }
        return ALT_TEXT_FOR_ROW_COLUMN_INDEX;
    }

    function nextTabElement() {
        if (isAlternativeKeyboardNavigationWithTTS()) {
            nextGapOrRow();
        } else {
            nextGap();
        }
    }

    function nextGapOrRow() {
        const nextX = findNextGapColumnInRow(presenter.keyboardState.y, presenter.keyboardState.x);
        if (nextX === presenter.keyboardState.x) {
            nextRow();
        } else {
            presenter.keyboardState.x = nextX;
            updateGapIndex();
            selectKeyboardNavigationElement();
        }
    }

    function nextGap() {
        if (presenter.isGapFocused) return;
        if (presenter.keyboardState.gapIndex + 1 &lt; presenter.gapElements.length) {
            presenter.keyboardState.gapIndex += 1;
            const gap = presenter.gapElements[presenter.keyboardState.gapIndex];
            presenter.keyboardState.x = gap.x;
            presenter.keyboardState.y = gap.y;
            selectKeyboardNavigationElement();
        }
    }

    function prevTabElement() {
        if (isAlternativeKeyboardNavigationWithTTS()) {
            prevGapOrRow();
        } else {
            prevGap();
        }
    }

    function prevGapOrRow() {
        let foundGapOrRow = false;
        let prevX = findPrevColumnInRow(presenter.keyboardState.y, presenter.keyboardState.x);
        if (prevX === presenter.keyboardState.x) {
            const prevNavigableRowIndex = findPrevNavigableRowIndexForAlternativeTTSNavigation(presenter.keyboardState.y)
            if (prevNavigableRowIndex !== presenter.keyboardState.y) {
                foundGapOrRow = true;
                presenter.keyboardState.y = prevNavigableRowIndex;
                if (prevNavigableRowIndex === DESCRIPTION_OF_OPERATION_ROW_INDEX) {
                    prevX = DESCRIPTION_OF_OPERATION_COLUMN_INDEX;
                } else {
                    prevX = findPrevColumnInRow(prevNavigableRowIndex);
                }
            }
        } else {
            foundGapOrRow = true;
        }

        if (foundGapOrRow) {
            presenter.keyboardState.x = prevX;
            updateGapIndex();
            selectKeyboardNavigationElement();
        }
    }

    function findNextNavigableRowIndexForAlternativeTTSNavigation(currentRowIndex) {
        for (let nextRowIndex = currentRowIndex + 1; nextRowIndex &lt; presenter.tableElements.length; nextRowIndex++) {
            if (isRowNavigableInAltTTSNavigation(nextRowIndex)) {
                return nextRowIndex;
            }
        }
        return currentRowIndex;
    }

    function findPrevNavigableRowIndexForAlternativeTTSNavigation(currentRowIndex) {
        for (let prevRowIndex = currentRowIndex - 1; prevRowIndex &gt;= DESCRIPTION_OF_OPERATION_ROW_INDEX; prevRowIndex--) {
            if (isRowNavigableInAltTTSNavigation(prevRowIndex)) {
                return prevRowIndex;
            }
        }
        return DESCRIPTION_OF_OPERATION_ROW_INDEX;
    }

    function isRowNavigableInAltTTSNavigation(rowIndex) {
        for (let i = 0; i &lt; presenter.rowsStatusesOfNavigabilityInAltTTSNavigation.length; i++) {
            if (rowIndex === presenter.rowsStatusesOfNavigabilityInAltTTSNavigation[i].rowIndex) {
                return presenter.rowsStatusesOfNavigabilityInAltTTSNavigation[i].isNavigable;
            }
        }
        return false;
    }

    function prevGap() {
        if (presenter.isGapFocused) return;
        if (presenter.keyboardState.gapIndex - 1 &gt; -1 &amp;&amp; presenter.gapElements.length &gt; 0) {
            presenter.keyboardState.gapIndex -= 1;
            const gap = presenter.gapElements[presenter.keyboardState.gapIndex];
            presenter.keyboardState.x = gap.x;
            presenter.keyboardState.y = gap.y;
            selectKeyboardNavigationElement();
        }
    }

    function updateGapIndex() {
        presenter.keyboardState.gapIndex = NO_GAP_INDEX;
        for (let i = 0; i &lt; presenter.gapElements.length; i++) {
            const gapElement = presenter.gapElements[i];
            if (presenter.keyboardState.x === gapElement.x &amp;&amp; presenter.keyboardState.y === gapElement.y) {
                presenter.keyboardState.gapIndex = i;
                break;
            }
        }
    }

    function enter() {
        if (isAlternativeKeyboardNavigationWithTTS() || presenter.keyboardState.y === DESCRIPTION_OF_OPERATION_ROW_INDEX) {
            return;
        }

        const $gap = presenter.tableElements[presenter.keyboardState.y][presenter.keyboardState.x].find('input.writing-calculations-input');
        if ($gap.length &gt; 0) {
            presenter.isGapFocused = true;
            $gap.addClass(presenter.KEYBOARD_SELECTED_CLASS);
            $gap.focus();
        }
    }

    function escape(event) {
        if (!isAlternativeKeyboardNavigationWithTTS() &amp;&amp; presenter.isGapFocused) {
            if (event) {
                event.stopPropagation();
                event.preventDefault();
            }
            const $gap = presenter.tableElements[presenter.keyboardState.y][presenter.keyboardState.x].find('input.writing-calculations-input');
            if ($gap.length &gt; 0) {
                setTimeout(()=&gt;{
                    presenter.isGapFocused = false;
                }, 0);
                $gap.removeClass(presenter.KEYBOARD_SELECTED_CLASS);
                $gap.blur();
            }
        } else {
            exitModuleInTTS();
        }
    }

    function exitModuleInTTS() {
        presenter.isGapFocused = false;
        presenter.keyboardState.x = DESCRIPTION_OF_OPERATION_COLUMN_INDEX;
        presenter.keyboardState.y = DESCRIPTION_OF_OPERATION_ROW_INDEX;
        presenter.keyboardState.gapIndex = NO_GAP_INDEX;
        selectKeyboardNavigationElement();
    }

    function selectKeyboardNavigationElement() {
        if (isAlternativeKeyboardNavigationWithTTS()) {
            document.activeElement.blur();
        }

        presenter.$view.find('.'+presenter.KEYBOARD_SELECTED_CLASS).removeClass(presenter.KEYBOARD_SELECTED_CLASS);
        if (presenter.keyboardState.y === DESCRIPTION_OF_OPERATION_ROW_INDEX) {
            return;
        }
        if (isAlternativeKeyboardNavigationWithTTS()) {
            if (presenter.keyboardState.x === ALT_TEXT_FOR_ROW_COLUMN_INDEX) {
                presenter.tableElements[presenter.keyboardState.y][0].parent().addClass(presenter.KEYBOARD_SELECTED_CLASS);
            } else {
                presenter.isGapFocused = true;
                const $gap = presenter.gapElements[presenter.keyboardState.gapIndex].el.find('input.writing-calculations-input');
                const gap = $gap[0];
                gap.focus();
                gap.setSelectionRange(gap.value.length, gap.value.length);
                $gap.addClass(presenter.KEYBOARD_SELECTED_CLASS);
            }
        } else {
            presenter.tableElements[presenter.keyboardState.y][presenter.keyboardState.x].children().first().addClass(presenter.KEYBOARD_SELECTED_CLASS);
        }
    }

    function isAlternativeKeyboardNavigationWithTTS() {
        return presenter.useAlternativeTTSNavigation &amp;&amp; presenter.isWCAGOn;
    }

    function readSelectedElement() {
        presenter.readSelectedElement(presenter.keyboardState.x, presenter.keyboardState.y);
    }

    presenter.getCellTitle = function (x, y) {
        var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        var title = "";
        do {
            var mod = x % 26;
            title = alphabet[mod] + " " + title;
            x -= mod;
            x = x / 26 - 1;
        } while (x &gt;= 0);
        title += (y+1);
        return title;
    }

    presenter.readSelectedElement = function(x, y) {
        if (!presenter.isWCAGOn) return;
        let data = [];
        if (y &lt; 0) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.descriptionOfOperation, presenter.langTag));
        } else if (presenter.useAlternativeTTSNavigation &amp;&amp; x === ALT_TEXT_FOR_ROW_COLUMN_INDEX) {
            let navigableRowsNumber = 0;
            for (let rowIndex = 0; rowIndex &lt;= presenter.keyboardState.y; rowIndex++) {
                if (isRowNavigableInAltTTSNavigation(rowIndex)) {
                    navigableRowsNumber++;
                }
            }
            data.push(window.TTSUtils.getTextVoiceObject(presenter.rowsAltTexts[navigableRowsNumber-1], presenter.langTag));
        } else {
            if (!presenter.useAlternativeTTSNavigation) {
                data.push(window.TTSUtils.getTextVoiceObject(presenter.getCellTitle(x,y)));
            }

            const $container = presenter.tableElements[y][x].children().first();
            const containerType = presenter.getContainerType($container);
            let input, inputVoices;

            switch(containerType) {
                case presenter.ELEMENT_TYPE.NUMBER:
                    data.push(window.TTSUtils.getTextVoiceObject($container.html(), presenter.langTag));
                    break;
                case presenter.ELEMENT_TYPE.SYMBOL:
                    const value = $container.data('value');
                    if (value === "*") {
                        data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.multiplicationSign));
                    } else if (value === ":" || value === ")") {
                        data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.divisionSign));
                    } else if (value === "+") {
                        data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.additionSign));
                    } else if (value === "-") {
                        data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.subtractionSign));
                    } else if (value === "#") {
                        data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.equalsSign));
                    }
                    break;
                case presenter.ELEMENT_TYPE.EMPTY_BOX:
                    data.push(window.TTSUtils.getTextVoiceObject(createGapInfoForTTS(x, y, false)));
                    input = $container.find('input');
                    if (input.length &gt; 0) {
                        inputVoices = presenter.getTextVoicesFromInput(input);
                        data = data.concat(inputVoices);
                    }
                    break;
                case presenter.ELEMENT_TYPE.LINE:
                    data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.line));
                    break;
                case presenter.ELEMENT_TYPE.HELP_BOX:
                    data.push(window.TTSUtils.getTextVoiceObject(createGapInfoForTTS(x, y, true)));
                    input = $container.find('input');
                    if (input.length &gt; 0) {
                        inputVoices = presenter.getTextVoicesFromInput(input);
                        data = data.concat(inputVoices);
                    }
                    break;
                default:
                    break;
            }

            const nextSibling = presenter.tableElements[y][x].next();
            if (nextSibling.length &gt; 0 &amp;&amp; presenter.getContainerType(nextSibling.children().first()) == presenter.ELEMENT_TYPE.DOT) {
                data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.decimalSeparator));
            }
        }
        presenter.speak(data);
    }

    function createGapInfoForTTS(x, y, isHelpBox) {
        let gapInfo = isHelpBox ? presenter.speechTexts.additionalGap : presenter.speechTexts.gap;
        if (presenter.useAlternativeTTSNavigation) {
            gapInfo += (getGapIndexInRow(x, y) + 1);
        }
        return gapInfo;
    }

    function getGapIndexInRow(x, y) {
        for (let i = 0; i &lt; presenter.gapElements.length; i++) {
            const gapElement = presenter.gapElements[i];
            if (x === gapElement.x &amp;&amp; y === gapElement.y) {
                return gapElement.gapInRowIndex;
            }
        }
    }

    presenter.getTextVoicesFromInput = function(inputElement) {
        var data =[];
        var value = inputElement.val();
        if (value.length == 0) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.empty));
        } else {
            data.push(window.TTSUtils.getTextVoiceObject(value, presenter.langTag));
        }
        if (inputElement.hasClass('correct')) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.correct));
        } else if (inputElement.hasClass('incorrect')) {
            data.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.wrong));
        }
        return data;
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function (data) {
        if (data.length == 0) return;
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);

        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.isDeactivationBlocked = function() {
        return presenter.isGapFocused &amp;&amp; !isAlternativeKeyboardNavigationWithTTS();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="YouTube_Addon" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="HTTPS" nameLabel="YouTube_Addon_property_https" type="boolean"/>
		<property name="ID" nameLabel="YouTube_Addon_property_id" type="string"/>
		<property name="URL" nameLabel="YouTube_Addon_property_url" type="string"/>
        <property name="Offline message" nameLabel="YouTube_Addon_property_offline_message" type="string"/>
        <property name="Disable Fullscreen" nameLabel="YouTube_Addon_property_disable_fullscreen" type="boolean"/>
		<property name="Time start" nameLabel="YouTube_Addon_property_time_start" type="integer"/>
		<property name="Autoplay" nameLabel="YouTube_Addon_property_autoplay" type="boolean"/>
	</model>
<css/><view/><preview/><presenter> function AddonYouTube_Addon_create(){
    var presenter = function() {
    };

    presenter.playerController = null;
    presenter.ismLibro = false;
 
    function createVideoThumbnailAsync(videoID, viewContainer, addonWidth, addonHeight) {

        var feedURL = "https://mauthor.com/api/v2/youtube/thumbnail?video_id=" + videoID;

        $.when($.get(feedURL)).then(function (jsonResponse) {
            if (jsonResponse.items.length &gt; 0) {
                var thumbnails = jsonResponse.items[0].snippet.thumbnails;
                var thumbnailURL = thumbnails.maxres.url;
                var thumbnailElement = document.createElement("img");
                $(thumbnailElement).attr('src', thumbnailURL);
                viewContainer.html(thumbnailElement);
                $(thumbnailElement).css({
                    width: addonWidth + 'px',
                    height: addonHeight + 'px'
                });
            }
        });
    }
 
    function showErrorMessage(viewContainer, errorMessage) {
        var errorElement = document.createElement('p');
        $(errorElement).text(errorMessage);
        viewContainer.html(errorElement);
    }

     function doesConnectionExist() {
         if (!presenter.ismLibro) return true;

         var xhr = new ( window.ActiveXObject || XMLHttpRequest )( "Microsoft.XMLHTTP" );

         //YouTube API key is generated in lorepocorporate google account
         xhr.open( "HEAD", "https://mauthor.com/api/v2/youtube/", false );

         try {
             xhr.send();
             return ( xhr.status &gt;= 200 &amp;&amp; xhr.status &lt; 300 || xhr.status === 304 );
         } catch (error) {
             return false;
         }
     }

    function presenterLogic(view, model, isPreview) {
        var width = model.Width;
        var height = model.Height;
        presenter.isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);
        presenter.isVisibleByDefault = presenter.isVisible;
        presenter.disableFullscreen = ModelValidationUtils.validateBoolean(model['Disable Fullscreen']);

        var isAutoplay = ModelValidationUtils.validateBoolean(model['Autoplay']);
        var timeStart = presenter.decodeTimeStart(model['Time start'] || "");
        var viewContainer = $(view);
        var decodedVideoID = presenter.decodeVideoID(model.URL, model.ID);

        presenter.$view = $(view);

        if (decodedVideoID.isError) {
            showErrorMessage(viewContainer, decodedVideoID.errorMessage);
            return;
        }
        if (!timeStart.isValid) {
            showErrorMessage(viewContainer, "Incorrect video time start - value must be number greater than 0");
            return;
        }

        if (isPreview) {
            createVideoThumbnailAsync(decodedVideoID.videoID, viewContainer, width, height);
        } else {
            presenter.setVisibility(presenter.isVisible);
            var src = '${protocol}://www.youtube.com/embed/${video_id}';
            src = src.replace("${video_id}", decodedVideoID.videoID);

            //Protocol (HTTP or HTTPS)
            var myProtocol = window.location.protocol;
            myProtocol = myProtocol.replace(":","");
            var httsStr = model.HTTPS;
            var protocol = httsStr === 'True' ? 'https' : 'http';

            if (myProtocol == 'https' || protocol == 'http') {
                src = src.replace("${protocol}", myProtocol);
            } else {
                src = src.replace("${protocol}", protocol);
            }

            var url = src + presenter.getUrlParams(isAutoplay, timeStart.value);

            var iframe = document.createElement('iframe');
            $(iframe).attr('id', 'ytIframe');
            $(iframe).attr('frameborder', '0');
            $(iframe).attr('src', url);
            $(iframe).attr('width', parseInt(width, 10) + 'px');
            $(iframe).attr('height', parseInt(height, 10) + 'px');

            if(!presenter.disableFullscreen){
                $(iframe).attr("allowfullscreen","allowfullscreen");
            }

            if(doesConnectionExist()){
                viewContainer.html(iframe);
            }else{
                var offlineDiv = document.createElement('div');
                $(offlineDiv).addClass('offline-message');
                if(model['Offline message']){
                    $(offlineDiv).text(model['Offline message']);
                    viewContainer.html(offlineDiv);
                }else{
                    $(offlineDiv).text('No connection to the Internet');
                    viewContainer.html(offlineDiv);
                }
            }
        }
    }
 
    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };
 
    presenter.run = function(view, model) {
        presenterLogic(view, model, false);
    };
 
    // Return -1 if any error occurs
    presenter.decodeVideoID = function(URL, ID) {
        //Encoding video ID from URL if ID is not given
        var videoID = "";
        var prefix = "";
        var methodResult = {
            videoID : -1,
            isError: false,
            errorMessage : ''
        };
 
        if(ID !== '') {
            if (ID.search(/[^a-zA-Z0-9_-]/gm) === -1) {
                methodResult.videoID = ID;
 
                return methodResult;
            } else {
                methodResult.isError = true;
                methodResult.errorMessage = "Video ID seems to be incorrect!";
 
                return methodResult;
            }
        }
 
        if(URL === '') {
            methodResult.isError = true;
            methodResult.errorMessage = "Neither video ID nor URL was given!";
 
            return methodResult;
        }
 
        var ampersandIndex = URL.indexOf("&amp;");
        var youtubeIndex = URL.indexOf("youtube.com/watch?v=");
 
        if(youtubeIndex == -1) {
            // There is a possibility that user enters shorted URL
            youtubeIndex = URL.indexOf("youtu.be/");
 
            if(youtubeIndex == -1) {
                methodResult.isError = true;
                methodResult.errorMessage = "URL seems to be incorrect!";
 
                return methodResult;
            }
 
            prefix = ".be/";
        } else {
            prefix = "?v=";
        }
 
        var startIndex = URL.indexOf(prefix) + prefix.length;
        if (startIndex == URL.length) {
            methodResult.isError = true;
            methodResult.errorMessage = "URL seems to be incorrect. It must contain video ID!";
 
            return methodResult;
        }
 
        if(ampersandIndex == -1) {
            videoID = URL.substring(startIndex);
        } else {
            videoID = URL.substring(startIndex, ampersandIndex);
        }
 
        methodResult.videoID = videoID;
 
        return methodResult;
    };

    presenter.decodeTimeStart = function (timeStart) {
        if (ModelValidationUtils.isStringEmpty(timeStart.trim())) {
            return {
                isValid: true,
                value: 0
            };
        }

        return ModelValidationUtils.validatePositiveInteger(timeStart);
    };

    presenter.getUrlParams = function (isAutoplay, timeStart) {
        var autoplay = isAutoplay ? 1 : 0;
        return `?enablejsapi=1&amp;start=${timeStart}&amp;autoplay=${autoplay}&amp;mute=${autoplay}`;
    };

     presenter.setVisibility = function (isVisible) {
         presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
     };

     presenter.setPlayerController = function (controller) {
         presenter.playerController = controller;
         var context = controller.getContextMetadata();
         if (context != null &amp;&amp; "ismLibro" in context) {
             presenter.ismLibro = context["ismLibro"];
         }

     };

     presenter.show = function() {
         presenter.setVisibility(true);
         presenter.isVisible = true;
     };

     presenter.hide = function() {
         presenter.setVisibility(false);
         presenter.isVisible = false;
     };

     // This function takes argument as function's name from iframe API:
     // https://developers.google.com/youtube/iframe_api_reference?hl=pl#Functions
     function callPlayer(func, args) {
         presenter.$view.find("iframe")[0].contentWindow.postMessage(JSON.stringify({
             'event': 'command',
             'func': func,
             'args': args || []
         }), "*");
     }

     presenter.stop = function() {
         callPlayer('stopVideo');
         callPlayer('seekTo', [0, true]);
     };

     presenter.executeCommand = function(name, params) {
         var commands = {
             'show': presenter.show,
             'hide': presenter.hide,
             'stop': presenter.stop
         };

         Commands.dispatch(commands, name, params, presenter);
     };

     presenter.reset = function(){
         presenter.isVisible = presenter.isVisibleByDefault;
         presenter.setVisibility(presenter.isVisibleByDefault);
     };

     presenter.getState = function () {
         return JSON.stringify({
             isVisible: presenter.isVisible
         });
     };

     presenter.upgradeStateForVisibility = function(state) {
         if (state.isVisible === undefined) {
             state.isVisible = true;
         }

         return state;
     };

     presenter.upgradeState = function (parsedState) {
         parsedState = presenter.upgradeStateForVisibility(parsedState);

         return parsedState;
     };

     presenter.setState = function(state) {
         if (ModelValidationUtils.isStringEmpty(state)) {
             return;
         }

         var parsedState = presenter.upgradeState(JSON.parse(state));

         presenter.isVisible = parsedState.isVisible;

         presenter.setVisibility(presenter.isVisible);
     };
 
    return presenter;
}
/**
 * YouTube Addon
 * Version 1.6
 * Last update: 23-08-2016
 */
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Zoom" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property name="Mode" nameLabel="Zoom_property_mode" type="{Targeted area, Area around a clicked point}"/>
    </model>
<css>.addon_Zoom {
    pointer-events: none;
}

.ic_page.zoom-cursor-zoom-in {
    cursor: zoom-in;
    cursor: -moz-zoom-in;
    cursor: -webkit-zoom-in;
}

.zoom-wrapper{
    width: -webkit-fill-available;
    height: -webkit-fill-available;
}

.zoom-wrapper.zoom-wrapper-highlight {
    border: dashed red;
}

.zoom-wrapper .zoom-button-container{
    position: absolute;
    pointer-events: auto;
}

.zoom-wrapper.targeted-area .zoom-button-container{
    bottom: 0px;
    right: 0px;
}

.zoomed-area-container{
    position: fixed;
    z-index: 102;
    pointer-events: none;
}

.zoomed-area-container .zoom-out-button-container{
    position: absolute;
    bottom: 4px;
    right: 4px;
    pointer-events: auto;
    z-index: 2;
}

.zoom-wrapper .zoom-button-container .zoom-button {
    background-image: url('resources/zoom_in.svg');
    width: 55px;
    height: 55px;
}

.zoomed-area-container .zoom-out-button-container .zoom-out-button {
    background-image: url('resources/zoom_out.svg');
    width: 55px;
    height: 55px;
}

.zoom-wrapper .zoom-button-container .zoom-button,
.zoomed-area-container .zoom-out-button-container .zoom-out-button {
    background-position: center;
    background-repeat: no-repeat;
    background-color: #3F51B5;
    background-size: 65%;
    border-radius: 50%;
    text-align: center;
    cursor: pointer;
}

.zoom-wrapper .zoom-button-container .zoom-button.selected {
   	background-color: #2196F3;
}
</css><view>&lt;div class="zoom-wrapper"&gt;
    &lt;div class="zoom-button-container"&gt;
        &lt;div class="zoom-button"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="zoom-wrapper"&gt;
    &lt;div class="zoom-button-container"&gt;
        &lt;div class="zoom-button"&gt;&lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonZoom_create() {
    var presenter = function() {};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.configuration = null;
    presenter.isVisible = true;
    presenter.scaleModifier = 1.0;
    presenter.zoomedAreaContainer = null;
    presenter.zoomingTimeoutID = null;

    presenter.DEFAULT_ZOOM = 2;
    presenter.ERROR_CODES = {
        "SIZE_1": "Addon must fit within page size limits",
    };

    presenter.MODE = {
        "Targeted area": "TARGETED_AREA",
        "Area around a clicked point": "AREA_AROUND_CLICKED_POINT",
        DEFAULT: "Targeted area"
    };

    presenter.CSS_CLASSES = {
        ZOOM_WRAPPER: "zoom-wrapper",
        TARGETED_AREA: "targeted-area",
        AREA_AROUND_CLICKED_POINT: "area-around-clicked-point",
        ZOOM_WRAPPER_HIGHLIGHT: "zoom-wrapper-highlight",
        ZOOM_BUTTON_CONTAINER: "zoom-button-container",
        ZOOM_BUTTON: "zoom-button",
        SELECTED: "selected",
        ZOOMED_AREA_CONTAINER: "zoomed-area-container",
        ZOOM_OUT_BUTTON_CONTAINER: "zoom-out-button-container",
        ZOOM_OUT_BUTTON: "zoom-out-button",
        ZOOM_IN_CURSOR: "zoom-cursor-zoom-in",
        ZOOM_ZOOMED_IN: "zoom-zoomed-in",
        IC_PAGE: "ic_page",
        IC_FOOTER: "ic_footer",
        IC_PAGE_PANEL: "ic_page_panel",
        IWB_ZOOM_IN: "iwb-zoom-in",
        IWB_ZOOM_OUT: "iwb-zoom-out",
        MODULES_GROUP: "modules_group",
    };

    presenter.DEFAULT_TTS_PHRASES = {
        ZOOM_IN: "Zoom in",
        ACTIVATED: "Zoom in",
        ZOOM_OUT: "Zoom out",
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;

        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener("ResizeWindow", this);
    };

    presenter.onEventReceived = function(eventName) {
        switch (eventName) {
            case "ResizeWindow":
                setFinalScaleInformation();
                break;
        }
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model);
    };

    presenter.run = function(view, model) {
        presenter.presenterLogic(view, model);
        
        if (!presenter.configuration.isValid) {
            return;
        }
        zoom.init({ elementToZoom: findPage() });
        presenter.addHandlers();
    };

    presenter.presenterLogic = function(view, model) {
        presenter.view = view;
        presenter.$view = $(view);

        const upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return;
        }
        presenter.addModeCSSClassName();
        presenter.setVisibility(presenter.configuration.isVisible);
    };

    presenter.upgradeModel = function(model) {
        return presenter.upgradeModelPropertyMode(model);
    };

    presenter.upgradeModelPropertyMode = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["Mode"]) {
            upgradedModel["Mode"] = "Targeted area";
        }

        return upgradedModel;
    };

    presenter.validateModel = function(model) {
        const mode = ModelValidationUtils.validateOption(presenter.MODE, model["Mode"]);

        const validatedSize = validateSizeRelativeToPageSize(mode);
        if (!validatedSize.isValid) {
            return validatedSize;
        }

        return {
            isValid: true,
            ID: model.ID,
            width: parseInt(model["Width"], 10),
            height: parseInt(model["Height"], 10),
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]),
            mode: mode
        };
    };

    function validateSizeRelativeToPageSize(mode) {
        if (mode !== "TARGETED_AREA") {
            return getCorrectObject();
        }

        const page = findPage();
        let left = presenter.view.offsetLeft;
        let top = presenter.view.offsetTop;
        if (presenter.view.parentElement.classList.contains(presenter.CSS_CLASSES.MODULES_GROUP)) {
            left += presenter.view.parentElement.offsetLeft;
            top += presenter.view.parentElement.offsetTop;
        }
        if (left &lt; 0 || left + presenter.view.offsetWidth &gt; page.offsetWidth) {
            return getErrorObject("SIZE_1");
        }
        if (top &lt; 0 || top + presenter.view.offsetHeight &gt; page.offsetHeight) {
            return getErrorObject("SIZE_1");
        }
        return getCorrectObject();
    }

    function getErrorObject (ec) { return { isValid: false, errorCode: ec }; }
    function getCorrectObject (v) { return { isValid: true, value: v }; }

    presenter.addModeCSSClassName = function () {
        const zoomWrapper = findInView(presenter.CSS_CLASSES.ZOOM_WRAPPER);
        if (!zoomWrapper) {
            return;
        }
        const modeCSSClassName = isTargetedAreaModeActive()
            ? presenter.CSS_CLASSES.TARGETED_AREA
            : presenter.CSS_CLASSES.AREA_AROUND_CLICKED_POINT;
        zoomWrapper.classList.add(modeCSSClassName);
    };

    function showZoomButtonContainer() {
        findInView(presenter.CSS_CLASSES.ZOOM_BUTTON_CONTAINER).style.display = "block";
    }

    function hideZoomButtonContainer() {
        findInView(presenter.CSS_CLASSES.ZOOM_BUTTON_CONTAINER).style.display = "none";
    }

    presenter.executeCommand = function (name, params) {
        const commands = {
            "show": presenter.show,
            "hide": presenter.hide,
            "zoomIn": presenter.zoomIn,
            "zoomInArea": presenter.zoomInArea,
            "zoomOut": presenter.zoomOut,
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.addHandlers = function () {
        if (isTargetedAreaModeActive()) {
            if (!MobileUtils.isMobileUserAgent(window.navigator.userAgent)) {
                addMouseOverZoomButtonListener();
            }
        } else {
            findPage().addEventListener("click", presenter.pageCallback);
        }
        findInView(presenter.CSS_CLASSES.ZOOM_BUTTON).addEventListener("click", presenter.zoomButtonCallback);

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();
    };

    presenter.removeHandlers = function () {
        if (!isTargetedAreaModeActive()) {
            findPage()?.removeEventListener("click", presenter.pageCallback);
        }
        findInView(presenter.CSS_CLASSES.ZOOM_BUTTON).removeEventListener("click", presenter.zoomButtonCallback);
    };

    function addMouseOverZoomButtonListener() {
        const zoomButton = findInView(presenter.CSS_CLASSES.ZOOM_BUTTON);
        const zoomWrapper = findInView(presenter.CSS_CLASSES.ZOOM_WRAPPER);

        zoomButton.addEventListener("mouseover", function (event) {
            zoomWrapper.classList.add(presenter.CSS_CLASSES.ZOOM_WRAPPER_HIGHLIGHT);
        });
        zoomButton.addEventListener("mouseout", function (event) {
            zoomWrapper.classList.remove(presenter.CSS_CLASSES.ZOOM_WRAPPER_HIGHLIGHT);
        });
    }

    presenter.pageCallback = function (event) {
        event.stopPropagation();
        if (!isEnabledZoomInCursor()) {
            return;
        }
        const pageRect = findPage().getBoundingClientRect();
        const scaleInfo = presenter.playerController.getScaleInformation();
        const x = (event.clientX - pageRect.x) / scaleInfo.baseScaleX;
        const y = (event.clientY - pageRect.y) / scaleInfo.baseScaleY;
        zoomIn(x, y);
    };

    presenter.zoomButtonCallback = function (event) {
        event.stopPropagation();
        if (isZoomed()) {
            return;
        }

        if (isTargetedAreaModeActive()) {
            zoomInOwnArea();
        } else {
            if (isEnabledZoomInCursor()) {
                enableZoomInCursor(false);
            } else {
                enableZoomInCursor(true);
            }
        }
    };

    presenter.zoomIn = function (x, y) {
        if (Array.isArray(x) &amp;&amp; x.length === 2 &amp;&amp; y === undefined) {
            y = parseInt(x[1]);
            x = parseInt(x[0]);
        }
        zoomIn(x, y);
    };

    function zoomIn(x, y) {
        if (isZoomed()) {
            return;
        }

        presenter.scaleModifier = presenter.DEFAULT_ZOOM;
        setFinalScaleInformation();

        const page = findPage();
        const adjustedPosition = adjustSelectedPointPositionToPageBoundaries(x, y);

        zoom.to({
            x: adjustedPosition.x,
            y: adjustedPosition.y,
            scale: presenter.scaleModifier,
            topWindowHeight: page.offsetHeight,
            topWindowWidth: page.offsetWidth,
            elementToZoom: page
        });

        preventOfIncludingScrollOffsetToTransformOrigin();
        createZoomedAreaContainer();
        enableZoomInCursor(false);
        hideZoomButtonContainer();
    }

    function zoomInOwnArea() {
        let left = presenter.view.offsetLeft;
        let top = presenter.view.offsetTop;
        if (presenter.view.parentElement.classList.contains(presenter.CSS_CLASSES.MODULES_GROUP)) {
            left += presenter.view.parentElement.offsetLeft;
            top += presenter.view.parentElement.offsetTop;
        }
        zoomInArea(
            left,
            top,
            presenter.view.offsetWidth,
            presenter.view.offsetHeight
        );
    }

    presenter.zoomInArea = function (x, y, width, height) {
        if (Array.isArray(x) &amp;&amp; x.length === 4 &amp;&amp; y === undefined &amp;&amp; width === undefined &amp;&amp; height === undefined) {
            height = parseInt(x[3]);
            width = parseInt(x[2]);
            y = parseInt(x[1]);
            x = parseInt(x[0]);
        }
        const modifiedAttributes = adjustZoomInAreaCommandToRestOfFunctionality(x, y, width, height);
        zoomInArea(modifiedAttributes.left, modifiedAttributes.top, modifiedAttributes.width, modifiedAttributes.height);
    };

    /**
     * Adjust the data given by a zoomInArea command to rest of functionality.
     *
     * Prevent also, from zooming the area outside the page boundaries.
     *
     * @method adjustZoomInAreaCommandToRestOfFunctionality
     * @param x:number Center point X coordinate
     * @param y:number Center point Y coordinate
     * @param width:number Width of area to zoom
     * @param height:number Height of area to zoom
     * @return {{left: number, top: number, width: number, height: number}} adjusted data
     */
    function adjustZoomInAreaCommandToRestOfFunctionality(x, y, width, height) {
        const page = findPage();

        let left = x - width / 2;
        let top = y - height / 2;

        const pageWidth = page.offsetWidth;
        width = Math.min(width, pageWidth);
        const minLeft = 0;
        const maxLeft = pageWidth - width;
        left = Math.min(Math.max(left, minLeft), maxLeft);

        const pageHeight = page.offsetHeight;
        height = Math.min(height, pageHeight);
        const minTop = 0;
        const maxTop = pageHeight - height;
        top = Math.min(Math.max(top, minTop), maxTop);

        return {left, top, width, height};
    }

    function zoomInArea(left, top, width, height) {
        if (isZoomed()) {
            return;
        }

        const page = findPage();
        const topWindowHeight = page.offsetHeight;
        const topWindowWidth = page.offsetWidth;
        presenter.scaleModifier = calculateScaleForArea(width, height, topWindowWidth, topWindowHeight);
        setFinalScaleInformation();

        const adjustedPosition = adjustSelectedAreaPositionToPageBoundaries(left, top, width, height);

        zoom.to({
            x: adjustedPosition.left,
            y: adjustedPosition.top,
            width: width,
            height: height,
            topWindowHeight: topWindowHeight,
            topWindowWidth: topWindowWidth,
            elementToZoom: page
        });

        preventOfIncludingScrollOffsetToTransformOrigin();
        createZoomedAreaContainer();
        enableZoomInCursor(false);
        hideZoomButtonContainer();
    }

    function calculateScaleForArea(areaWidth, areaHeight, topWindowWidth, topWindowHeight) {
        let scale = Math.max( Math.min( topWindowWidth / areaWidth, topWindowHeight / areaHeight ), 1 );
        if (Math.min( topWindowWidth / areaWidth, topWindowHeight / areaHeight ) &lt;= 1){
            scale = Math.min( topWindowWidth / areaWidth, topWindowHeight / areaHeight )+1;
        }
        return scale;
    }

    function preventOfIncludingScrollOffsetToTransformOrigin() {
        findPage().style.transformOrigin = "0px 0px";
    }

    /**
     * Adjust the position data of a selected point, which is the center point of the zoomed area, so that the zoomed
     * area is within the boundaries of the page.
     *
     * @method adjustSelectedPointPositionToPageBoundaries
     * @param centerX:number Center point X coordinate
     * @param centerY:number Center point Y coordinate
     * @return {{x: number, y: number}} Center point position for zoomed area
     */
    function adjustSelectedPointPositionToPageBoundaries(centerX, centerY) {
        const page = findPage();

        const pageWidth = page.offsetWidth;
        const zoomedAreaWidth = pageWidth / presenter.scaleModifier;
        const halfOfZoomedAreaWidth = zoomedAreaWidth / 2;
        const minX = halfOfZoomedAreaWidth;
        const maxX = pageWidth - halfOfZoomedAreaWidth;
        centerX = Math.min(Math.max(centerX, minX), maxX);

        const pageHeight = page.offsetHeight;
        const zoomedAreaHeight = pageHeight / presenter.scaleModifier;
        const halfOfZoomedAreaHeight = zoomedAreaHeight / 2;
        const minY = halfOfZoomedAreaHeight;
        const maxY = pageHeight - halfOfZoomedAreaHeight;
        centerY = Math.min(Math.max(centerY, minY), maxY);

        return {x: centerX, y: centerY};
    }

    /**
     * Adjust the position data of a selected area, which is the left top apex point of the zoomed area,
     * so that the zoomed area is within the boundaries of the page.
     *
     * @method adjustSelectedAreaPositionToPageBoundaries
     * @param left:number Left top apex point X coordinate
     * @param top:number Left top apex point Y coordinate
     * @param width:number Width of area to zoom
     * @param height:number Height of area to zoom
     * @return {{left: number, top: number}} Left top apex point position for zoomed area
     */
    function adjustSelectedAreaPositionToPageBoundaries(left, top, width, height) {
        const page = findPage();

        const pageWidth = page.offsetWidth;
        const zoomedAreaWidth = pageWidth / presenter.scaleModifier;
        const halfOfZoomedAreaWidth = zoomedAreaWidth / 2;
        const centerMinX = halfOfZoomedAreaWidth;
        const centerMaxX = pageWidth - halfOfZoomedAreaWidth;
        let centerX = left + width / 2;
        centerX = Math.min(Math.max(centerX, centerMinX), centerMaxX);
        left = centerX - width / 2;

        const pageHeight = page.offsetHeight;
        const zoomedAreaHeight = pageHeight / presenter.scaleModifier;
        const halfOfZoomedAreaHeight = zoomedAreaHeight / 2;
        const centerMinY = halfOfZoomedAreaHeight;
        const centerMaxY = pageHeight - halfOfZoomedAreaHeight;
        let centerY = top + height / 2;
        centerY = Math.min(Math.max(centerY, centerMinY), centerMaxY);
        top = centerY - height / 2;

        return {left, top};
    }

    function setFinalScaleInformation() {
        const scaleInfo = presenter.playerController.getScaleInformation();
        presenter.playerController.setFinalScaleInformation({
            scaleX: scaleInfo.baseScaleX * presenter.scaleModifier,
            scaleY: scaleInfo.baseScaleY * presenter.scaleModifier
        });
    }

    function createZoomedAreaContainer() {
        presenter.zoomedAreaContainer = document.createElement("div");
        presenter.zoomedAreaContainer.classList.add(presenter.CSS_CLASSES.ZOOMED_AREA_CONTAINER);
        presenter.zoomedAreaContainer.style.visibility = "hidden";

        const buttonContainer = document.createElement("div");
        buttonContainer.classList.add(presenter.CSS_CLASSES.ZOOM_OUT_BUTTON_CONTAINER);
        presenter.zoomedAreaContainer.append(buttonContainer);

        const button = document.createElement("div");
        button.classList.add(presenter.CSS_CLASSES.ZOOM_OUT_BUTTON);
        button.addEventListener("click", presenter.zoomOutButtonCallback);
        buttonContainer.append(button);

        document.body.append(presenter.zoomedAreaContainer);

        const timeout = 1000; // zoom.TRANSITION_DURATION + 200
        presenter.zoomingTimeoutID = setTimeout(() =&gt; {
            findPage().classList.add(presenter.CSS_CLASSES.ZOOM_ZOOMED_IN);
            setUpZoomedAreaContainerAfterZoomIn();
            presenter.zoomingTimeoutID = null;
        }, timeout);
    }

    function setUpZoomedAreaContainerAfterZoomIn() {
        const scrollElements = findScrollElements();
        let maxScrollTop = 0;
        try {
            for (let i = 0; i &lt; scrollElements.length; i++) {
                let elementScrollTop = scrollElements[i].scrollTop();
                maxScrollTop = Math.max(elementScrollTop, maxScrollTop);
            }
        } catch(e) {}
        adjustZoomedAreaContainerCSS(undefined, maxScrollTop);
        addHandlersNeededWhenZoomed();
        presenter.zoomedAreaContainer.style.visibility = "visible";
    }

    function findScrollElements() {
        const $defaultScrollElement = $(window.parent.document);
        const $mCourserScrollElement = $defaultScrollElement.find('#lesson-view &gt; div &gt; div');
        const $mAuthorMobileScrollElement = $(window);
        const $mCourserMobileScrollElement = $("#content-view");
        const $mLibroDesktopScrollElement = $($("#_icplayerContent")[0]?.shadowRoot.querySelector(".inner-scroll"));
        return [
            $defaultScrollElement, $mCourserScrollElement, $mAuthorMobileScrollElement,
            $mCourserMobileScrollElement, $mLibroDesktopScrollElement
        ];
    }

    function isScreenOrientationChangeHandlerNeeded() {
        const isIOS = MobileUtils.isSafariMobile(window.navigator.userAgent);
        const isSafari = navigator.userAgent.toLowerCase().indexOf('safari/') &gt; -1;
        return isIOS &amp;&amp; isSafari &amp;&amp; screen &amp;&amp; screen.orientation;
    }

    function addHandlersNeededWhenZoomed() {
        addEscHandlerNeededWhenZoomed();
        addOnScrollHandlersNeededWhenZoomed();
        if (isScreenOrientationChangeHandlerNeeded()) {
            addOnScreenOrientationChangeHandler();
        }
    }

    function addOnScrollHandlersNeededWhenZoomed() {
        const scrollElements = findScrollElements();
        try {
            for (let i = 0; i &lt; scrollElements.length; i++) {
                scrollElements[i].scroll(adjustZoomedAreaContainerCSS);
            }
        } catch (e) {}
    }

    function addEscHandlerNeededWhenZoomed() {
        document.body.addEventListener( "keyup", keyUpHandler);
    }

    function addOnScreenOrientationChangeHandler() {
        screen.orientation.addEventListener("change", adjustZoomedAreaContainerCSS);
    }

    function keyUpHandler(event) {
        const escKeyCode = 27;
        if (event.keyCode === escKeyCode) {
            presenter.zoomOut();
        }
    }

    function removeHandlersNeededWhenZoomed() {
        removeEscHandlerNeededWhenZoomed();
        removeOnScrollHandlersNeededWhenZoomed();
        if (isScreenOrientationChangeHandlerNeeded()) {
            removeOnScreenOrientationChangeHandler();
        }
    }

    function removeOnScrollHandlersNeededWhenZoomed() {
        const scrollElements = findScrollElements();
        try {
            for (let i = 0; i &lt; scrollElements.length; i++) {
                scrollElements[i][0].removeEventListener("scroll", adjustZoomedAreaContainerCSS);
            }
        } catch (e) {}
    }

    function removeEscHandlerNeededWhenZoomed() {
        document.body.removeEventListener( "keyup", keyUpHandler);
    }

    function removeOnScreenOrientationChangeHandler() {
        screen.orientation.removeEventListener("change", adjustZoomedAreaContainerCSS);
    }

    function removeZoomedAreaContainer() {
        if (presenter.zoomedAreaContainer) {
            presenter.zoomedAreaContainer.remove();
            presenter.zoomedAreaContainer = null;
        }
    }

    function adjustZoomedAreaContainerCSS(event, scrollTop) {
        if (!presenter.zoomedAreaContainer) {
            return;
        }
        if (!scrollTop) {
            scrollTop = $(this).scrollTop();
        }
        setFinalScaleInformation(); // Prevent scale information from being overwritten

        const isInIframe = window.parent.location !== window.location;
        if (isInIframe) {
            adjustHeightAndVerticalPositionWhenInIframe(scrollTop);
        } else {
            adjustHeightAndVerticalPositionWhenNotInIframe();
        }
        adjustWidthAndHorizontalPosition();
        adjustZoomedAreaContainerTransform();
    }

    function adjustHeightAndVerticalPositionWhenInIframe(scrollTop) {
        const scaleInfo = presenter.playerController.getScaleInformation();
        const iframeSize = {
            height: window.document.body.clientHeight,
            innerHeight: window.parent.innerHeight,
            offsetTop: window.frameElement.offsetTop,
            scaleX: window.frameElement.getBoundingClientRect().height / window.frameElement.offsetHeight
        };

        let newHeight = iframeSize.height;
        if (iframeSize.innerHeight &lt; iframeSize.height + iframeSize.offsetTop) {
            newHeight = iframeSize.innerHeight;
            if (scrollTop &lt; iframeSize.offsetTop) {
                newHeight -= iframeSize.offsetTop - scrollTop;
            }
        }
        newHeight = newHeight / scaleInfo.scaleY;
        let newTop = 0;
        if (scrollTop &gt; iframeSize.offsetTop) {
            newTop = scrollTop - iframeSize.offsetTop;
        }

        presenter.zoomedAreaContainer.style.height = newHeight / iframeSize.scaleX + "px";
        presenter.zoomedAreaContainer.style.top = newTop / iframeSize.scaleX + "px";
    }

    function adjustHeightAndVerticalPositionWhenNotInIframe() {
        const scaleInfo = presenter.playerController.getScaleInformation();
        const pagePanelRect = findPagePanel().getBoundingClientRect();
        const footerPagePanel = findFooterPagePanel();
        let footerHeight = 0;
        if (footerPagePanel) {
            footerHeight = footerPagePanel.getBoundingClientRect().height;
        }

        const topOffset = pagePanelRect.top;
        const bottomOffset = window.innerHeight - (pagePanelRect.height + footerHeight) - topOffset;

        let newHeight = pagePanelRect.height + footerHeight;
        let newTop = topOffset;
        if (topOffset &lt; 0) {
            newTop = 0;
            newHeight += topOffset;
        }
        if (bottomOffset &lt; 0) {
            newHeight += bottomOffset;
        }

        presenter.zoomedAreaContainer.style.height = newHeight / scaleInfo.scaleY + "px";
        presenter.zoomedAreaContainer.style.top = newTop + "px";
    }

    function adjustWidthAndHorizontalPosition() {
        const scaleInfo = presenter.playerController.getScaleInformation();
        const pagePanelRect = findPagePanel().getBoundingClientRect();

        const leftOffset = pagePanelRect.left;
        const rightOffset = window.innerWidth - pagePanelRect.width - leftOffset;
        let newWidth = pagePanelRect.width;
        let newLeft = leftOffset;
        if (leftOffset &lt; 0) {
            newLeft = 0;
            newWidth += leftOffset;
        }
        if (rightOffset &lt; 0) {
            newWidth += rightOffset;
        }

        presenter.zoomedAreaContainer.style.width = newWidth / scaleInfo.scaleX + "px";
        presenter.zoomedAreaContainer.style.left = newLeft + "px";
    }

    function adjustZoomedAreaContainerTransform() {
        const scaleInfo = presenter.playerController.getScaleInformation();

        presenter.zoomedAreaContainer.style.transform = "scale(" + scaleInfo.scaleX + ", " + scaleInfo.scaleY + ")";
        presenter.zoomedAreaContainer.style.transformOrigin = "left top";
    }

    function isZoomed() {
        const icPagePanel = findPagePanel();
        const isIWBToolbarActive = (icPagePanel.classList.contains(presenter.CSS_CLASSES.IWB_ZOOM_IN) ||
            icPagePanel.classList.contains(presenter.CSS_CLASSES.IWB_ZOOM_OUT));
        if (isIWBToolbarActive) {
            return true;
        }

        const scaleInfo = presenter.playerController.getScaleInformation();
        return ((scaleInfo.baseScaleX !== scaleInfo.scaleX || scaleInfo.baseScaleY !== scaleInfo.scaleY) ||
            document.body.getElementsByClassName(presenter.CSS_CLASSES.ZOOMED_AREA_CONTAINER).length &gt; 0
        );
    }

    function enableZoomInCursor(enable) {
        const page = findPage();
        const zoomButton = findInView(presenter.CSS_CLASSES.ZOOM_BUTTON);
        if (enable) {
            page.classList.add(presenter.CSS_CLASSES.ZOOM_IN_CURSOR);
            zoomButton.classList.add(presenter.CSS_CLASSES.SELECTED);
        } else {
            page.classList.remove(presenter.CSS_CLASSES.ZOOM_IN_CURSOR);
            zoomButton.classList.remove(presenter.CSS_CLASSES.SELECTED);
        }
    }

    function isEnabledZoomInCursor() {
        return findPage().classList.contains(presenter.CSS_CLASSES.ZOOM_IN_CURSOR);
    }

    presenter.zoomOutButtonCallback = function (event) {
        event.stopPropagation();
        presenter.zoomOut();
    };

    presenter.zoomOut = function () {
        if (presenter.scaleModifier === 1.0) {
            return;
        }
        presenter.scaleModifier = 1.0;
        setFinalScaleInformation();
        removeZoomedAreaContainer();
        if (presenter.zoomingTimeoutID) {
            clearTimeout(presenter.zoomingTimeoutID);
            presenter.zoomingTimeoutID = null;
        }
        zoom.out();
        removeHandlersNeededWhenZoomed();
        findPage()?.classList.remove(presenter.CSS_CLASSES.ZOOM_ZOOMED_IN);
        showZoomButtonContainer();
    };

    presenter.getState = function() {
        return JSON.stringify({
            isVisible: presenter.isVisible
        });
    };

    presenter.setState = function(state) {
        if (ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        const parsedState = JSON.parse(state);
        presenter.setVisibility(parsedState.isVisible);
    };

    function isTargetedAreaModeActive() {
        return presenter.configuration.mode === "TARGETED_AREA";
    }

    function findPage() {
        return presenter.view.closest('.' + presenter.CSS_CLASSES.IC_PAGE);
    }

    function findPagePanel() {
        return findPage().closest('.' + presenter.CSS_CLASSES.IC_PAGE_PANEL);
    }

    function findFooterPagePanel() {
        return document.body.querySelector('.' + presenter.CSS_CLASSES.IC_FOOTER)?.closest('.' + presenter.CSS_CLASSES.IC_PAGE_PANEL);
    }

    function findInView(cssClass) {
        return presenter.view.querySelector('.' + cssClass);
    }

    presenter.reset = function() {
        presenter.zoomOut();
        presenter.setVisibility(presenter.configuration.isVisible);
        enableZoomInCursor(false);
    };

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }

        if (zoom) {
            presenter.zoomOut();
            zoom.destroy();
        }

        presenter.removeHandlers();
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Zoom_Image" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
    <model>
        <property displayName="Full Screen image" name="Full Screen image" nameLabel="Zoom_Image_property_full_screen_image" type="image"/>
        <property displayName="Page image" name="Page image" nameLabel="Zoom_Image_property_page_image" type="image"/>
        <property name="Alternative text" nameLabel="Zoom_Image_property_alternative_text" type="string"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="Zoom_image_property_lang_attribute" type="string"/>
         <property displayName="Speech texts" name="speechTexts" nameLabel="Zoom_image_property_speech_texts" type="staticlist">
            <property name="Closed" nameLabel="Zoom_image_property_closed" type="staticrow">
                <property name="Closed" nameLabel="Zoom_image_property_closed" type="string"/>
            </property>
        </property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
    </model>
<css>.addon_Zoom_Image .content {
    position: relative;
}

.addon_Zoom_Image .content div.icon {
    position: absolute;
    background-color: #adff2f;
    width: 30px;
    height: 30px;
    bottom: 0;
    right: 0;
}

img.big {
    display: table;
    margin: auto;
}

.close-button-ui-dialog {
    display: table;
    top: 0;
    right: 0;
    position: absolute;
    height: 25px;
    width: 25px;
    background: rgba(255, 255, 255, 0.15);
    border-radius: 5px;
    text-align: center;
    font-family: monospace;
    cursor: pointer;
    color: #FFF;
}

.close-cross-ui-dialog {
    font-size: 15px;
    display: table-cell;
    vertical-align: middle;
}
</css><view>&lt;div class="content"&gt;
    &lt;div class="icon"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="content"&gt;
    &lt;div class="icon"&gt;&lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddonZoom_Image_create() {

    var presenter = function() {};
    var eventBus;
    var playerController = null;
    var isWCAGOn = false;
    var oldFocus = null;
    var backgroundColorStyle;
    var opacity;

    function setup_presenter() {
        presenter.view = null;
        presenter.$view = null;
        presenter.$image = null;
        presenter.removeOpenedDialog = null;
        presenter.bigImageCreated = null;
        presenter.bigImageLoaded = null;
        presenter.createPopUp = null;
        presenter.isOpened = false;
    }

    setup_presenter();

    function setSmallImage(url) {
        var $image = $('&lt;img class="small"&gt;');
        $image.attr("src", url);
        $image.attr("height", presenter.configuration.height);
        $image.attr("width", presenter.configuration.width);
        $image.attr("alt", presenter.configuration.alt);
        presenter.$view.find("div.content").append($image);
        if ( presenter.configuration.isTabindexEnabled) {$image.attr('tabindex', '0');}
    }

    presenter.ERROR_CODES = {
        IMAGE01: "Property Full Screen image and Page image cannot be empty"
    };

    function returnErrorObject(ec) { return { isValid: false, errorCode: ec }; }

    function returnCorrectObject(v) { return { isValid: true, value: v }; }

    function parseImage(image) {
        if (ModelValidationUtils.isStringWithPrefixEmpty(image, "/file/")) {
            return returnErrorObject("IMAGE01");
        }

        return returnCorrectObject(image);
    }

    presenter.setPlayerController = function(controller) {
        playerController = controller;
        eventBus = controller.getEventBus();
    };

    presenter.validateModel = function(model) {

        setSpeechTexts(model['speechTexts']);
        var validatedBigImage = parseImage(model["Full Screen image"]);
        if (!validatedBigImage.isValid) {
            return returnErrorObject(validatedBigImage.errorCode);
        }

        var validatedSmallImage = parseImage(model["Page image"]);
        if (!validatedSmallImage.isValid) {
            return returnErrorObject(validatedSmallImage.errorCode);
        }

        var isVisible = ModelValidationUtils.validateBoolean(model['Is Visible']);

        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']);

        return {
            bigImage: validatedBigImage.value,
            smallImage: validatedSmallImage.value,
            ID: model.ID,
            width: parseInt(model["Width"], 10),
            height: parseInt(model["Height"], 10),
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            isValid: true,
            alt: model['Alternative text'],
            isTabindexEnabled: isTabindexEnabled,
            langTag: model['langAttribute']
        }
    };

    presenter.presenterLogic = function(view, model, isPreview) {
        presenter.view = view;
        presenter.$view = $(view);

        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        if (!presenter.configuration.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            return false;
        }

        setSmallImage(presenter.configuration.smallImage);

        if (!isPreview) {
            presenter.eventType = MobileUtils.isMobileUserAgent(navigator.userAgent) ? "touchend" : "click";
            presenter.$view.find(".icon").on(presenter.eventType, presenter.createPopUp);
            presenter.setVisibility(presenter.configuration.isVisible);
        }

        MutationObserverService.createDestroyObserver(presenter.configuration.ID, presenter.destroy, presenter.view);
        MutationObserverService.setObserver();

        return false;
    };

    presenter.upgradeModel = function (model) {
        return presenter.upgradeFrom_01(model);
    };

    presenter.upgradeFrom_01 = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {
                Closed: {Closed: 'closed'}
            };
        }

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = '';
        }

        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            closed:  'closed'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            closed: getSpeechTextProperty(speechTexts['Closed']['Closed'], presenter.speechTexts.closed)
        };
    }

    presenter.destroy = function (event) {
        if (event.target !== presenter.view) {
            return;
        }
        if (presenter.isOpened) {
            presenter.removeOpenedDialog();
            presenter.$image.dialog("close");
        }
        presenter.unbindEvents();

        setup_presenter();
        setup_presenter = null;
    };

    presenter.unbindEvents = function () {
        presenter.$view.find(".icon").off(presenter.eventType, presenter.createPopUp);
        if (presenter.$image !== null) {
            if (presenter.isOpened) {
                presenter.$view.find('.close-button-ui-dialog').off('click', presenter.removeOpenedDialog);
            }
            presenter.$image.off();
        }
    };

    presenter.run = function(view, model) {
        presenter.$view = $(view);
        presenter.presenterLogic(view, model, false);
    };

    presenter.createPreview = function(view, model) {
        presenter.presenterLogic(view, model, true);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
        presenter.configuration.isVisible = isVisible;
    };

    function calculateImageSizeAndPosition(image) {
        var $player;
        if (document.getElementById('_icplayer') != null){
            $player = $('#_icplayer');
        } else{
            $player = $('.ic_page_panel');
        }

        var dialog = {};
        var x = image.width;
        var y = image.height;
        var playerWidth = $player[0].getBoundingClientRect()?.width || $player.width();
        var playerHeight = $player[0].getBoundingClientRect()?.height || $player.height();
        var xProportion = x / playerWidth;
        var yProportion = y / playerHeight;

        if (xProportion &lt; 1 &amp;&amp; yProportion &lt; 1) {
            dialog.width = x;
            dialog.height = y;
        } else if (xProportion &gt; yProportion) {
            dialog.width = playerWidth;
            dialog.height = y / xProportion;
        } else {
            dialog.height = playerHeight;
            dialog.width = x / yProportion;
        }

        dialog.left = window.PositioningUtils.calculateLeftForPopupToBeCentred(dialog.width);
        dialog.top = window.PositioningUtils.calculateTopForPopupToBeCentred(dialog.height);

        return dialog;
    }

    function sendEvent(value) {
        var eventData = {
            source: presenter.configuration.ID,
            value: value
        };
        eventBus.sendEvent('ValueChanged', eventData);
    }

    presenter.removeOpenedDialog = function (e) {
        $('.ui-widget-overlay').css("opacity", opacity);
        $('.ui-widget-overlay').css("background", backgroundColorStyle);
        if(e) {
            e.preventDefault();
            e.stopPropagation();
        }

        $(".zoom-image-wraper").remove();
        $(".big").remove();
        sendEvent(0);
        presenter.isOpened = false;
    };

    presenter.bigImageCreated = function() {
        var $close = $('&lt;div class="close-button-ui-dialog"&gt;');
        $close.on('click', presenter.removeOpenedDialog);

        $(this).parents(".ui-dialog").append($close);

        var $closeCross= $('&lt;div class="close-cross-ui-dialog"&gt;');
        $closeCross.html('&amp;times;');
        $(this).parents(".ui-dialog").children(".close-button-ui-dialog").append($closeCross);

        $(this).parents(".ui-dialog:first").find(".ui-dialog-titlebar").css("display", "none");
        $(this).parents(".ui-dialog").css("padding", 0);
        $(this).parents(".ui-dialog").css("border", 0);
        $(this).parents(".ui-dialog:first").find(".ui-dialog-content").css("padding", 0);
        sendEvent(1);
    };

    presenter.bigImageLoaded = function(){
        var dialogSizeAndPosition = calculateImageSizeAndPosition(this);

        presenter.$image.appendTo(presenter.$view);

        if(!oldFocus &amp;&amp; isWCAGOn &amp;&amp; $.browser.mozilla) {
            // This hack is meant to prevent issues between TTS and NVDA on Firefox
            // When the dialog is created, jquery.ui changes browser focus, causing NVDA to speak
            // simultaneously with TTS. In order to prevent that, jQuery.focus() function is temporarily disabled
            // and then restored after dialog has been created
            oldFocus = $.fn.focus;
            $.fn.focus = function () {
                return this;
            };
        }
        presenter.$image.dialog({
            height: dialogSizeAndPosition.height,
            width: dialogSizeAndPosition.width,
            position: [dialogSizeAndPosition.left, dialogSizeAndPosition.top],
            modal: true,
            resizable: false,
            draggable: false,
            show: {
                effect: "fade",
                duration: 1000
            },
            create: presenter.bigImageCreated,
            open: function(event, ui) {
                opacity = $('.ui-widget-overlay').css("opacity");
                backgroundColorStyle = $('.ui-widget-overlay').css("background");
                $('.ui-widget-overlay').css("background", "black");
                $('.ui-widget-overlay').css("opacity", "0.7");
                $('.ui-widget-overlay').on(presenter.eventType, presenter.removeOpenedDialog);

                const dialogElement = $(event.target).closest('.ui-dialog')[0];
                const dialogElementBoundingClientRect = dialogElement.getBoundingClientRect();
                const newWidth = dialogElementBoundingClientRect.width;
                const newHeight = dialogElementBoundingClientRect.height;
                dialogElement.style.left = window.PositioningUtils.calculateLeftForPopupToBeCentred(newWidth) + "px";
                dialogElement.style.top = window.PositioningUtils.calculateTopForPopupToBeCentred(newHeight) + "px";
            }
        });
        presenter.$image.parent().wrap("&lt;div class='zoom-image-wraper'&gt;&lt;/div&gt;");
        presenter.$image.on(presenter.eventType, presenter.removeOpenedDialog);

        if(oldFocus) {
            // Restoring jQuery.focus() after the hack meant to prevent issues between TTS and NVDA on Firefox
            $.fn.focus = oldFocus;
            oldFocus = null;
        }
    };

    presenter.createPopUp = function createPopUp(e) {
        if(e) {
            e.preventDefault();
            e.stopPropagation();
        }
        presenter.$image = $("&lt;img class='big' src='" + presenter.configuration.bigImage + "' alt='"+presenter.configuration.alt+"'&gt;");
        presenter.$image.on("load", presenter.bigImageLoaded);
        presenter.isOpened = true;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
    };

    presenter.show = function () {
        presenter.setVisibility(true);
    };

    presenter.reset = function() {
        presenter.configuration.isVisibleByDefault ? presenter.show() : presenter.hide();
    };

    presenter.getState = function () {
        return JSON.stringify({
            isVisible: presenter.configuration.isVisible
        });
    };

    presenter.upgradeStateForVisibility = function (parsedState) {
        if (parsedState.isVisible == undefined) {
            parsedState.isVisible = true;
        }

        return parsedState;
    };

    presenter.upgradeState = function(parsedState) {
        return presenter.upgradeStateForVisibility(parsedState);
    };

    presenter.setState = function (state) {
        if (!state) {
            return;
        }

        var parsedState = JSON.parse(state),
            upgradedState = presenter.upgradeState(parsedState);

        presenter.setVisibility(upgradedState.isVisible);
    };

    presenter.keyboardController = function(keyCode, isShift, event) {
         if (keyCode == window.KeyboardControllerKeys.SPACE ||
             keyCode == window.KeyboardControllerKeys.ESC ||
             keyCode == window.KeyboardControllerKeys.ARROW_UP ||
             keyCode == window.KeyboardControllerKeys.ARROW_DOWN)
         {
             event.preventDefault();
         }

         if (keyCode === 13 &amp;&amp; !isShift) { // Enter button
            if (!presenter.isOpened) {
                presenter.createPopUp();
            }
            presenter.readAltText();
        }

        if (keyCode === 27 || keyCode === 9) { // ESC or TAB button
            presenter.removeOpenedDialog();
            presenter.$view.removeClass('ic_active_module');
            if (keyCode === 27) {
                presenter.readClosed();
            } else {
                playerController.getKeyboardController().moveActiveModule(isShift);
            }
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    }

    presenter.readAltText = function() {
        var speechVoices = [];
        speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.configuration.alt, presenter.configuration.langTag));
        speak(speechVoices);
    };

    presenter.readClosed = function() {
        var speechVoices = [];
        speechVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.closed));
        speak(speechVoices);
    };

    presenter.isEnterable = function() {return presenter.isOpened};

    presenter.getPrintableHTML = function (model, showAnswers) {
        model = presenter.upgradeModel(model);
        var $root = $('&lt;div&gt;&lt;/div&gt;');
        $root.attr('id',model.ID);
        $root.addClass('printable_addon_Zoom_Image');


        var $img = $('&lt;img&gt;&lt;/img&gt;');
        $img.attr('src', model['Full Screen image']);
        $root.append($img);
        return $root[0].outerHTML;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="crossword" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isLocalized="true" name="Crossword" nameLabel="crossword_property_crossword" type="text"/>
		<property name="Columns" nameLabel="crossword_property_columns" type="string"/>
		<property name="Rows" nameLabel="crossword_property_rows" type="string"/>
		<property name="Cell width" nameLabel="crossword_property_cell_width" type="string"/>
		<property name="Cell height" nameLabel="crossword_property_cell_height" type="string"/>
		<property name="Blank cells border color" nameLabel="crossword_property_blank_cells_border_color" type="string"/>
		<property name="Blank cells border style" nameLabel="crossword_property_blank_cells_border_style" type="{solid, dotted, dashed}"/>
		<property name="Blank cells border width" nameLabel="crossword_property_blank_cells_border_width" type="string"/>
		<property name="Letter cells border color" nameLabel="crossword_property_letter_cells_border_color" type="string"/>
		<property name="Letter cells border style" nameLabel="crossword_property_letter_cells_border_style" type="{solid, dotted, dashed}"/>
		<property name="Letter cells border width" nameLabel="crossword_property_letter_cells_border_width" type="string"/>
		<property name="Word numbers" nameLabel="crossword_property_word_numbers" type="{both, horizontal, vertical, none}"/>
		<property name="Marked column index" nameLabel="crossword_property_marked_column_index" type="string"/>
		<property name="Marked row index" nameLabel="crossword_property_marked_row_index" type="string"/>
        <property displayName="Block wrong answers" name="blockWrongAnswers" nameLabel="crossword_property_block_wrong_answers" type="boolean"/>
		<property displayName="Show all answers in gradual show answers mode" name="showAllAnswersInGradualShowAnswersMode" nameLabel="crossword_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
        <property displayName="Auto navigation" name="autoNavigation" nameLabel="crossword_property_auto_navigation" type="{Extended, Simple, Off}"/>
        <property displayName="Lang attribute" name="langAttribute" nameLabel="crossword_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="crossword_property_speech_texts" type="staticlist">
			<property name="Cell" nameLabel="crossword_speech_text_cell" type="staticrow">
                <property name="Cell" nameLabel="crossword_speech_text_cell" type="string"/>
            </property>
            <property name="Across" nameLabel="crossword_speech_text_across" type="staticrow">
                <property name="Across" nameLabel="crossword_speech_text_across" type="string"/>
            </property>
            <property name="Down" nameLabel="crossword_speech_text_down" type="staticrow">
                <property name="Down" nameLabel="crossword_speech_text_down" type="string"/>
            </property>
            <property name="Correct" nameLabel="crossword_speech_text_correct" type="staticrow">
                <property name="Correct" nameLabel="crossword_speech_text_correct" type="string"/>
            </property>
            <property name="Wrong" nameLabel="crossword_speech_text_wrong" type="staticrow">
                <property name="Wrong" nameLabel="crossword_speech_text_wrong" type="string"/>
            </property>
			<property name="Empty" nameLabel="crossword_speech_text_empty" type="staticrow">
                <property name="Empty" nameLabel="crossword_speech_text_empty" type="string"/>
            </property>
            <property name="Disabled" nameLabel="crossword_speech_text_disabled" type="staticrow">
                <property name="Disabled" nameLabel="crossword_speech_text_disabled" type="string"/>
            </property>
			<property name="OutOf" nameLabel="crossword_speech_text_out_of" type="staticrow">
                <property name="OutOf" nameLabel="crossword_speech_text_out_of" type="string"/>
            </property>
		</property>
        <property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
        <property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
	</model>
<css>.crossword_container {
    position: absolute;
    top: 50%;
    left: 50%;
}

.crossword_container .cell_container {
    float: left;
    position: relative;
}

.printable_crossword_container .printable_cell_container {
    float: left;
    position: relative;
}

.crossword_container .cell {
    position: absolute;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
}

.printable_crossword_container .printable_cell {
    position: absolute;
    top: 0px;
    left: 0px;
    right: 0px;
    bottom: 0px;
    display: flex;
    place-content: center;
}

.crossword_container .cell_letter {
    background: white;
}

.printable_crossword_container .printable_cell_letter {
    background: white;
}

.crossword_container .cell_valid {
    background: #00ff00;
}

.crossword_container .cell_invalid {
    background: #aa0000;
}

.crossword_container .cell_invalid input {
    color: #fff;
}

.crossword_container .cell_container_blank {
    background: black;
}

.printable_crossword_container .printable_cell_container_blank {
    background: black;
}

.crossword_container .cell_letter input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    text-align: center;
    background: transparent;
    border: none;
    -webkit-appearance: none;
}

.printable_crossword_container .printable_cell_letter_content {
    text-align: center;
    background: transparent;
    border: none;
    -webkit-appearance: none;
    text-transform: uppercase;
    margin-top: auto;
    margin-bottom: auto;
    font-size: large;
}

.crossword_container .cell .word_number {
    position: absolute;
    top: 2px;
    left: 2px;
    font-size: 6pt;
}

.printable_crossword_container .printable_cell .printable_word_number {
    position: absolute;
    top: 2px;
    left: 2px;
    font-size: 6pt;
}

.printable_crossword_container .printable_sign_valid {
    margin-top: auto;
    margin-bottom: auto;
    margin-left: 2px;
}

.printable_crossword_container .printable_sign_invalid {
    margin-top: auto;
    margin-bottom: auto;
    margin-left: 2px;
}

.printable_crossword_container .printable_sign_valid::after {
    content: " \2714";
}

.printable_crossword_container .printable_sign_invalid::after {
    content: " \2716";
}
</css><view/><preview/><presenter>function Addoncrossword_create(){
    var presenter = function() {};

    var playerController;
    var eventBus;
    var originalFieldValue = "";

    presenter.ID               = null;
    presenter.$view            = null;
    presenter.crossword        = null;

    presenter.rowCount         = null;
    presenter.columnCount      = null;
    presenter.cellHeight       = null;
    presenter.cellWidth        = null;
    presenter.maxScore         = null;
    presenter.isVisible        = true;
    presenter.showAllAnswersInGradualShowAnswersMode;
    presenter.isGradualShowAnswersActive = false;
    presenter.blankCellsBorderStyle  = "solid";
    presenter.blankCellsBorderWidth  = 0;
    presenter.blankCellsBorderColor  = "transparent";
    presenter.letterCellsBorderStyle = "solid";
    presenter.letterCellsBorderWidth = 0;
    presenter.letterCellsBorderColor = "transparent";
    presenter.wordNumbersHorizontal = false;
    presenter.wordNumbersVertical = false;
    presenter.disableAutomaticWordNumbering = false;
    presenter.blockWrongAnswers = false;
    presenter.markedColumnIndex = 0;
    presenter.markedRowIndex = 0;
    presenter.maxTabIndex = 0;
    presenter.areUserAnswersSaved = false;
    presenter.correctAnswers = [];

    presenter.printableController = null;
    presenter.printableState = null;
    presenter.printableStateMode = null;
    presenter.GSAcounter = null;

    var enableMoveToNextField = false;

    const DIRECTIONS = {
        NOT_SET: 0,
        HORIZONTAL: 1,
        VERTICAL: 2,
        TAB_INDEX: 3,
        NEXT_VERTICAL_ANSWER: 4
    }
    var currentDirection = DIRECTIONS.NOT_SET;

    const AUTO_NAVIGATION_OPTIONS = {
        OFF: 0,
        SIMPLE: 1,
        EXTENDED: 2
    }
    var autoNavigationMode = null;

    presenter.SPECIAL_KEYS = {
        BACKSPACE: 8,
        TAB: 9,
        ENTER: 13,
        SHIFT: 16,
        CTRL: 17,
        ESCAPE: 27,
        LEFT_ARROW: 37,
        UP_ARROW: 38,
        RIGHT_ARROW: 39,
        DOWN_ARROW: 40,
        DELETE: 46,
    };

    presenter.numberOfConstantLetters = 0;

    presenter.ERROR_MESSAGES = {
        ROWS_NOT_SPECIFIED:                          "Amount of rows is not specified",
        COLUMNS_NOT_SPECIFIED:                       "Amount of columns is not specified",
        INVALID_MARKED_COLUMN_INDEX:                 "Marked column index cannot be negative, use 0 to disable",
        INVALID_MARKED_ROW_INDEX:                    "Marked row index cannot be negative, use 0 to disable",
        CELL_WIDTH_NOT_SPECIFIED:                    "Cell width is not specified",
        CELL_HEIGHT_NOT_SPECIFIED:                   "Cell height is not specified",
        INVALID_BLANK_CELLS_BORDER_WIDTH:            "Blank cells border width must be greater on equal to 0",
        INVALID_LETTER_CELLS_BORDER_WIDTH:           "Letter cells border width must be greater on equal to 0",
        INVALID_AMOUNT_OF_ROWS_IN_CROSSWORD:         "Amount of lines (that act as rows) in the specified Crossword is different that amount of rows you have specified in Properties",
        INVALID_AMOUNT_OF_COLUMNS_IN_CROSSWORD:      "Amount of characters (that act as columns) in row %row% of specified Crossword is different that amount of columns you have specified in Properties",
        DOUBLED_EXCLAMATION_MARK:                    "You cannot type 2 exclamation marks in a row",
        LAST_CHARACTER_EXCLAMATION_MARK:             "You cannot type exclamation mark at the end of line",
        EXCLAMATION_MARK_BEFORE_EMPTY_FIELD:         "You cannot type exclamation mark before empty field",
        NOT_SUPPORTED_SELECTED_AUTO_NAVIGATION_MODE: "Selected auto navigation mode is not supported"
    };

    presenter.VALIDATION_MODE = {
        COUNT_SCORE: 0,
        SHOW_ERRORS: 1,
        CHECK_ANSWERS: 2
    };

    presenter.isModelValid = true;

    presenter.isWCAGOn = false;
    presenter.keyboardControllerObject = null;

    presenter.DEFAULT_TTS_PHRASES = {
        CELL: "cell",
        ACROSS: "across",
        DOWN: "down",
        CORRECT: "correct",
        WRONG: "wrong",
        EMPTY: "empty",
        DISABLED: "disabled",
        OUT_OFF: "out of",
    };

    presenter.CSS_CLASSES = {
        CELL: 'cell',
        CELL_BLANK: 'cell_blank',
        CELL_LETTER: "cell_letter",
        CELL_VALID: "cell_valid",
        CELL_INVALID: "cell_invalid",
        PRINTABLE: "printable",
        PRINTABLE_ADDON: "printable_addon_crossword",
        PRINTABLE_CELL_LETTER_CONTENT: "printable_cell_letter_content",
        PRINTABLE_SIGN_VALID: "printable_sign_valid",
        PRINTABLE_SIGN_INVALID: "printable_sign_invalid",
        PRINTABLE_CELL_VALID: "printable_cell_valid",
        PRINTABLE_CELL_INVALID: "printable_cell_invalid",
    };

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

    presenter.showErrorMessage = function(message, substitutions) {
        var errorContainer;
        if(typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.$view.html(errorContainer);
    };

    presenter.prepareGrid = function(model) {
        const elementPath = "div." + (isInPrintableStateMode() ? presenter.CSS_CLASSES.PRINTABLE + "_" : "") + "crossword_container";
        presenter.tabIndexBase = ($(elementPath).length * 5000) + 5000;
        presenter.maxScore = 0;
        presenter.crossword = [];

        var rows = model['Crossword'].split("\n");
        for(var i = 0; i &lt; presenter.rowCount; i++) {
            var r = [];
            var numberOfExclamationMarks = rows[i].match(/!/g) == null ? 0 : rows[i].match(/!/g).length;
            presenter.numberOfConstantLetters += numberOfExclamationMarks;
            for(var j = 0; j &lt; presenter.columnCount + numberOfExclamationMarks; j++) {
                if (rows[i][j] === '!') {
                    j++;
                    r.push('!' + capitalize(rows[i][j]));
                } else {
                    r.push(capitalize(rows[i][j]));
                }
            }

            presenter.crossword.push(r);
        }
    };

    presenter.isAnswerVisibleByDefault = function (answer) {
        return (answer.match(/!/g) || []).length * 2 === answer.length;
    }

    presenter.prepareAnswerHorizontal = function (row, column) {
        const position = { x: column, y: row };
        let answer = "";
        for(let i = column; i &lt; presenter.columnCount; i++){
            if (presenter.crossword[row][i] == ' ') {
                break;
            }
            answer = answer + presenter.crossword[row][i];
        }
        let answerData = {
            answer,
            position,
            isHorizontal: true
        };
        if (!presenter.isAnswerVisibleByDefault(answer))
            presenter.correctAnswers.push(answerData);
    }

    presenter.prepareAnswerVertical = function (row, column) {
        const position = { x: column, y: row };
        let answer = "";
        for (let i = row; i &lt; presenter.rowCount; i++){
            if (presenter.crossword[i][column] == ' '){
                break;
            }
            answer = answer + presenter.crossword[i][column];
        }
        let answerData = {
            answer,
            position,
            isHorizontal: false
        };
        if (!presenter.isAnswerVisibleByDefault(answer))
            presenter.correctAnswers.push(answerData);
    }

    presenter.prepareCorrectAnswers = function() {
        presenter.correctAnswers = [];

        for (let row = 0; row &lt; presenter.rowCount; row++) {
            for (let column = 0; column &lt; presenter.columnCount; column++){
                if(presenter.isHorizontalWordBegin(row, column)){
                    presenter.prepareAnswerHorizontal(row, column);
                }
                if(presenter.isVerticalWordBegin(row, column)){
                    presenter.prepareAnswerVertical(row, column);
                }
            }
        }
    }

    presenter.isHorizontalWordBegin = function(i, j) {
        if(!presenter.wordNumbersHorizontal) {
            return false;
        }

        return (
            // Skip empty cells
            presenter.crossword[i][j] != ' ' &amp;&amp;

                // We don't have a letter on the left
                (j === 0 ||  presenter.crossword[i][j-1] == ' ') &amp;&amp;

                // We do have a letter on the right
                (presenter.columnCount &gt; j+1 &amp;&amp; presenter.crossword[i][j+1] != ' '));
    };

    presenter.isVerticalWordBegin = function(i, j) {
        if(!presenter.wordNumbersVertical) {
            return false;
        }

        return (
            // Skip empty cells
            presenter.crossword[i][j] != ' ' &amp;&amp;

                // We don't have a letter above
                (i === 0 ||  presenter.crossword[i-1][j] == ' ') &amp;&amp;

                // We do have a letter below
                (presenter.rowCount &gt; i+1 &amp;&amp; presenter.crossword[i+1][j] != ' '));
    };

    function getPositionOfCellInputElement($cellInput) {
        return presenter.getPosition($cellInput.parent(''));
    }

    presenter.getPosition = function($elem) {
        function getPositionFrom(classes, dim) {
            return classes.reduce(function(res, currentElem) {
                return res === null ? currentElem.match(new RegExp(dim + "(\\d+)")) : res;
            }, null)[1];
        }

        var classes = $elem.attr('class').split(' ');

        return {
            x: parseInt(getPositionFrom(classes, 'cell_column_'), 10),
            y: parseInt(getPositionFrom(classes, 'cell_row_'), 10)
        }
    };

    var dictValues = function(dict) {
        var values = [];
        var keys = Object.keys(dict);
        keys.filter(function(key){
            values.push(dict[key])
        });
        return values;
    };

    presenter.SPECIAL_KEYS_CODES = dictValues(presenter.SPECIAL_KEYS);

    var validateSpecialKey = function(event) {
        if (presenter.SPECIAL_KEYS_CODES.indexOf(event.keyCode) &gt; -1 ||
            // Allow: dot
            (event.keyCode == 190) ||
            // Allow: Ctrl+A
            (event.keyCode == 65 &amp;&amp; event.ctrlKey === true) ||
            // Allow: end and home
            (event.keyCode &gt;= 35 &amp;&amp; event.keyCode &lt;= 36)) {
            // let it happen, don't do anything
            return true;
        }
        return false;
    };

    presenter.onCellInputKeyDown = function(event) {
        if (event.keyCode === presenter.SPECIAL_KEYS.ENTER) {
            return;
        }

        if ([
            presenter.SPECIAL_KEYS.BACKSPACE,
            presenter.SPECIAL_KEYS.LEFT_ARROW,
            presenter.SPECIAL_KEYS.RIGHT_ARROW,
            presenter.SPECIAL_KEYS.UP_ARROW,
            presenter.SPECIAL_KEYS.DOWN_ARROW,
            presenter.SPECIAL_KEYS.TAB,
        ].includes(event.keyCode)) {
            presenter.keyboardController(event.keyCode, event.shiftKey, event);
            return;
        }

        var $cellInput = $(event.target);
        if (originalFieldValue.length == 0) {
            originalFieldValue = $cellInput.val();
        }

        if (validateSpecialKey(event)) {
            return
        }

        $cellInput.css('color', 'rgba(0,0,0,0.0)');
        enableMoveToNextField = true;
    };

    presenter.onCellInputKeyUp = function(event) {
        var cellInput = event.target;
        var $cellInput = $(cellInput);
        $cellInput.css('color','');

        if (validateSpecialKey(event)) {
            return
        }

        if ($cellInput.val().length &gt; 1 &amp;&amp; originalFieldValue.length &gt; 0) {
            $cellInput.val($cellInput.val().replace(originalFieldValue,''));
        }
        originalFieldValue = '';

        cellInput.value = capitalize(cellInput.value);

        if (presenter.blockWrongAnswers) {
            var isCorrectValue
                = presenter.validateIsCorrectValueInCellInput(cellInput);
            if (!isCorrectValue) {
                presenter.keyboardControllerObject.speakWrong();
                enableMoveToNextField = false;
            }
        }

        if ($cellInput.val() &amp;&amp; enableMoveToNextField) {
            handleAutoNavigationMove(cellInput);
        }
    };

    presenter.onCellInputFocus = function(event) {
        event.target.select();
        var length = $(event.target).val().length;
        setCaretPosition(event.target, length + 1);
        if(length &gt; 1) {
            $(event.target).val($(event.target).val().substring(1, 2));
        }
        $(event.target).val(capitalize($(event.target).val()));
    };
 
    presenter.onCellInputMouseUp = function(event) {
        event.preventDefault();
    };

    presenter.onCellInputFocusOut = function(event) {
        var cellInput = event.target;
        var usersLetter = cellInput.value;
        var pos = getPositionOfCellInputElement($(cellInput));
        var correctLetter = presenter.crossword[pos.y][pos.x][0];
        var isOk = usersLetter === correctLetter;
        presenter.sendScoreEvent(pos, usersLetter, isOk);
        var score = isOk ? 1 : 0;
        if(score == 0 &amp;&amp; presenter.blockWrongAnswers){
            cellInput.value = "";
        }
        if (isOk) {
            const result = presenter.validateWord(pos);
            const isLetterInVerticalWord = result.verticalResult ?
                presenter.letterIsInWord(result.verticalResult.start, result.verticalResult.end, pos) : false;
            const isLetterInHorizontalWord = result.horizontalResult ?
                presenter.letterIsInWord(result.horizontalResult.start, result.horizontalResult.end, pos) : false;

            if (result.horizontalResult &amp;&amp; isLetterInHorizontalWord) {
                presenter.sendCorrectWordEvent(result.horizontalResult.word, result.horizontalResult.item);
            }
            if (result.verticalResult &amp;&amp; isLetterInVerticalWord) {
                presenter.sendCorrectWordEvent(result.verticalResult.word, result.verticalResult.item);
            }
        }
    };

    presenter.letterIsInWord = function (startPoint, endPoints, letterPosition) {
        const startCoordinates = startPoint.split(':');
        const endCoordinates = endPoints.split(':');
        const isInXAxis = +startCoordinates[0] &lt;= letterPosition.x &amp;&amp; +endCoordinates[0] &gt;= letterPosition.x;
        const isInYAxis = +startCoordinates[1] &lt;= letterPosition.y &amp;&amp; +endCoordinates[1] &gt;= letterPosition.y;

        return isInXAxis &amp;&amp; isInYAxis;
    };

    presenter.onCellClick = function(event) {
        presenter.resetDirection();
        event.stopPropagation();
    };

    function handleAutoNavigationMove(currentCellInput) {
        enableMoveToNextField = false;

        if (!presenter.isAutoNavigationInOffMode()) {
            presenter.analyzeDirectionOfMove(currentCellInput);
            presenter.updateDirectionOfMoveRelativeToAutoNavigationMode();
            presenter.moveInCurrentDirection(currentCellInput);
        }
    }

    presenter.validateIsCorrectValueInCellInput = function (currentCellInput) {
        var $currentCellInput = $(currentCellInput);
        var usersLetter = currentCellInput.value[0];
        var currentPosition = getPositionOfCellInputElement($currentCellInput);

        var correctLetter = presenter.crossword[currentPosition.y][currentPosition.x][0];
        if (usersLetter !== correctLetter) {
            presenter.sendScoreEvent(currentPosition, usersLetter, false);
            currentCellInput.value = '';
            return false;
        }
        return true;
    };

    presenter.isWordNumbersCorrect = function () {
        return presenter.wordNumbersHorizontal || presenter.wordNumbersVertical;
    }

    presenter.isWordOrientationOnlyHorizontal = function () {
        return presenter.wordNumbersHorizontal &amp;&amp; !presenter.wordNumbersVertical;
    }

    presenter.isWordOrientationOnlyVertical = function () {
        return presenter.wordNumbersVertical &amp;&amp; !presenter.wordNumbersHorizontal;
    }

    presenter.getWordBeginCellAtColumn = function (i) {
        return presenter.$view.find(`.cell_word_begin_vertical.cell_column_${i}`);
    }

    presenter.findNextColumn = function (currentPosition) {
        const nextColumnIndex = currentPosition.x + 1;

        for (let i = nextColumnIndex; i &lt;= presenter.columnCount; i++) {
            const $nextColumnWordBeginCell = presenter.getWordBeginCellAtColumn(i);

            if ($nextColumnWordBeginCell.length) {
                const position = presenter.getPosition($nextColumnWordBeginCell);
                if (isAnyOfBottomCellsEditable(position)) {
                    position.y =- 1;
                    return getNextBottomCellPosition(position);
                }
            }
        }
    }

    presenter.updateDirectionBasedOnWordOrientation = function () {
        if (presenter.isWordOrientationOnlyHorizontal()) {
            presenter.setHorizontalDirection();
        } else if (presenter.isWordOrientationOnlyVertical()) {
            presenter.setVerticalDirection();
        }
    }

    presenter.analyzeDirectionOfMove = function (currentCellInput) {
        var $currentCellInput = $(currentCellInput);
        var currentPosition = getPositionOfCellInputElement($currentCellInput);

        var rightElementPosition = calculateRightElementPosition(currentPosition);
        var isRightCellNotBlank = isPositionOfNotBlankCell(rightElementPosition);
        var rightCellsEditable = isAnyOfRightCellsEditable(currentPosition);

        var bottomElementPosition = calculateBottomElementPosition(currentPosition);
        var isBottomCellNotBlank = isPositionOfNotBlankCell(bottomElementPosition);
        var bottomCellsEditable = isAnyOfBottomCellsEditable(currentPosition);

        var topElementPosition = calculateTopElementPosition(currentPosition);
        var isTopCellNotBlank = isPositionOfNotBlankCell(topElementPosition);

        presenter.updateDirectionBasedOnWordOrientation();

        if (presenter.isHorizontalDirection()) {
            if (!rightCellsEditable) {
                presenter.setTabIndexDirection();
            }
            return;
        }
        if (presenter.isVerticalDirection()) {
            if (!bottomCellsEditable) {
                if (presenter.isWordOrientationOnlyVertical()) {
                    presenter.setNextVerticalAnswerDirection();
                } else {
                    presenter.setTabIndexDirection();
                }
            }
            return;
        }

        var rightCellInput = getCellInput(rightElementPosition);
        var bottomCellInput = getCellInput(bottomElementPosition);

        if (bottomCellsEditable &amp;&amp; !isRightCellNotBlank) {
            presenter.setVerticalDirection();
        } else if (rightCellsEditable &amp;&amp; !isTopCellNotBlank &amp;&amp; !isBottomCellNotBlank) {
            presenter.setHorizontalDirection();
        } else if (bottomCellsEditable &amp;&amp; !isTopCellNotBlank &amp;&amp; isRightCellNotBlank) {
            presenter.setVerticalDirection();
        } else if (bottomCellsEditable
            &amp;&amp; (isRightCellNotBlank &amp;&amp; !isCellInputElementEmpty(rightCellInput))
            &amp;&amp; (isBottomCellNotBlank &amp;&amp; isCellInputElementEmpty(bottomCellInput))) {
            presenter.setVerticalDirection();
        } else if (rightCellsEditable) {
            presenter.setHorizontalDirection();
        } else {
            presenter.setTabIndexDirection();
        }
    };

    function isAnyOfRightCellsEditable(position) {
        return !!getNextRightCellPosition(position);
    }

    function getNextRightCellPositionOfCellInput(cellInput, includeConstantCells = false) {
        const position = getPositionOfCellInputElement($(cellInput));
        return getNextRightCellPosition(position, includeConstantCells);
    }

    function getNextRightCellPosition(position, includeConstantCells = false) {
        const nextYPosition = position.y;
        var nextPosition;
        for (var nextXPosition = position.x + 1; nextXPosition &lt; presenter.columnCount; nextXPosition++) {
            nextPosition = {y: nextYPosition, x: nextXPosition};
            var isNextCellNotBlank = isPositionOfNotBlankCell(nextPosition);
            if (isNextCellNotBlank) {
                if (includeConstantCells || !isPositionOfConstantCell(nextPosition)) {
                    return nextPosition;
                }
            } else {
                return;
            }
        }
    }

    function isAnyOfLeftCellsEditable(position) {
        return !!getNextLeftCellPosition(position);
    }

    function getNextLeftCellPositionOfCellInput(cellInput, includeConstantCells = false) {
        const position = getPositionOfCellInputElement($(cellInput));
        return getNextLeftCellPosition(position, includeConstantCells);
    }

    function getNextLeftCellPosition(position, includeConstantCells = false) {
        const nextYPosition = position.y;
        var nextPosition;
        for (var nextXPosition = position.x - 1; nextXPosition &gt;= 0; nextXPosition--) {
            nextPosition = {y: nextYPosition, x: nextXPosition};
            var isNextCellNotBlank = isPositionOfNotBlankCell(nextPosition);
            if (isNextCellNotBlank) {
                if (includeConstantCells || !isPositionOfConstantCell(nextPosition)) {
                    return nextPosition;
                }
            } else {
                return;
            }
        }
    }

    function isAnyOfBottomCellsEditable(position) {
        return !!getNextBottomCellPosition(position);
    }

    function getNextBottomCellPositionOfCellInput(cellInput, includeConstantCells = false) {
        const position = getPositionOfCellInputElement($(cellInput));
        return getNextBottomCellPosition(position, includeConstantCells);
    }

    function getNextBottomCellPosition(position, includeConstantCells = false) {
        const nextXPosition = position.x;
        var nextPosition;
        for (var nextYPosition = position.y + 1; nextYPosition &lt; presenter.rowCount; nextYPosition++) {
            nextPosition = {y: nextYPosition, x: nextXPosition};
            var isNextCellNotBlank = isPositionOfNotBlankCell(nextPosition);
            if (isNextCellNotBlank) {
                if (includeConstantCells || !isPositionOfConstantCell(nextPosition)) {
                    return nextPosition;
                }
            } else {
                if (!presenter.isNextVerticalAnswerDirection())
                    return;
            }
        }
    }

    function isAnyOfTopCellsEditable(position) {
        return !!getNextTopCellPosition(position);
    }

    function getNextTopCellPositionOfCellInput(cellInput, includeConstantCells = false) {
        const position = getPositionOfCellInputElement($(cellInput));
        return getNextTopCellPosition(position, includeConstantCells);
    }

    function getNextTopCellPosition(position, includeConstantCells = false) {
        const nextXPosition = position.x;
        var nextPosition;
        for (var nextYPosition = position.y - 1; nextYPosition &gt;= 0; nextYPosition--) {
            nextPosition = {y: nextYPosition, x: nextXPosition};
            var isNextCellNotBlank = isPositionOfNotBlankCell(nextPosition);
            if (isNextCellNotBlank) {
                if (includeConstantCells || !isPositionOfConstantCell(nextPosition)) {
                    return nextPosition;
                }
            } else {
                return;
            }
        }
    }

    function getNextTabIndexEditableCellPositionOfCellInput(cellInput) {
        const newTabIndex = cellInput.tabIndex + 1;
        return getPositionOfCellInputElementWithTabIndex(newTabIndex);
    }

    function getPreviousTabIndexEditableCellPositionOfCellInput(cellInput) {
        const newTabIndex = cellInput.tabIndex - 1;
        return getPositionOfCellInputElementWithTabIndex(newTabIndex, true);
    }

    function getPositionOfCellInputElementWithTabIndex(tabIndex, isShiftDown = false) {
        if (tabIndex &lt; presenter.tabIndexBase || tabIndex &gt;= presenter.maxTabIndex) {
            return;
        }

        let newCellInputElement;
        if (!presenter.isWCAGOn) {
            newCellInputElement = presenter.getNewCellInputElement(tabIndex, isShiftDown);
        } else {
            newCellInputElement = findCellInputElement(tabIndex);
        }

        if (!newCellInputElement.length) {
            return;
        }

        var newPosition = getPositionOfCellInputElement($(newCellInputElement));
        if (!isPositionValid(newPosition)) {
            return;
        }

        return newPosition;
    }

    presenter.getNewCellInputElement = function (tabIndex, isShiftDown) {
        let newCellInputElement = findCellInputElement(tabIndex);
        let isDisabled = $(newCellInputElement).prop('disabled')
        let newTabIndex = tabIndex;
        while (newTabIndex &gt;= presenter.tabIndexBase &amp;&amp; isDisabled &amp;&amp; newTabIndex &lt; presenter.maxTabIndex) {
            newTabIndex = isShiftDown ? newTabIndex - 1 : newTabIndex + 1
            newCellInputElement = findCellInputElement(newTabIndex);
            isDisabled = $(newCellInputElement).prop('disabled');
        }

        return newCellInputElement;
    }

    function findCellInputElement(tabIndex) {
        return presenter.$view.find('[tabindex=' + tabIndex + ']');
    }

    presenter.updateDirectionOfMoveRelativeToAutoNavigationMode = function () {
        if (!presenter.isDirectionNotSet()
            &amp;&amp; (presenter.isAutoNavigationInOffMode()
                || (presenter.isAutoNavigationInSimpleMode()
                    &amp;&amp; (presenter.isTabIndexDirection()
                        || presenter.isNextVerticalAnswerDirection())))) {
            presenter.resetDirection();
        }
    }

    presenter.moveInCurrentDirection = function (currentCellInput) {
        if (presenter.isHorizontalDirection()) {
            moveInHorizontalDirection(currentCellInput);
        } else if (presenter.isVerticalDirection()) {
            moveInVerticalDirection(currentCellInput);
        } else if (presenter.isTabIndexDirection()) {
            moveInTabIndexDirection(currentCellInput);
        }  else if (presenter.isNextVerticalAnswerDirection()) {
            moveToNextVerticalAnswer(currentCellInput);
        }
    };

    function moveInVerticalDirection(startingCellInput) {
        const customEvent = createKeyboardNavigationEventForMove(startingCellInput);
        moveToNextBottomCellInput(customEvent);
    }

    function moveInHorizontalDirection(startingCellInput) {
        const customEvent = createKeyboardNavigationEventForMove(startingCellInput);
        moveToNextRightCellInput(customEvent);
    }

    function moveInTabIndexDirection(startingCellInput) {
        const customEvent = createKeyboardNavigationEventForMove(startingCellInput);
        moveToNextEditableTabIndexCellInput(customEvent);
    }

    function createKeyboardNavigationEventForMove(cellInput) {
        return {
            target: cellInput,
            preventDefault: function () {},
            stopPropagation: function () {},
        };
    }

    function focusCellInputUsingPosition(nextPosition) {
        $(getCellInput(nextPosition)).focus();
    }

    function moveToNextVerticalAnswer(currentCellInput) {
        const currentPosition = getPositionOfCellInputElement($(currentCellInput));
        const nextCellPosition = presenter.findNextColumn(currentPosition);
        if (!!nextCellPosition) {
            const nextCellInput = getCellInput(nextCellPosition);
            $(nextCellInput).focus();
        } else {
            blurCellInput(currentCellInput);
        }
    }

    function blurCellInput(cellInput) {
        $(cellInput).blur();
    }

    function calculateLeftElementPosition(oldPosition) {
        return {y: oldPosition.y, x: oldPosition.x - 1};
    }

    function calculateRightElementPosition(oldPosition) {
        return {y: oldPosition.y, x: oldPosition.x + 1};
    }

    function calculateTopElementPosition(oldPosition) {
        return {y: oldPosition.y - 1, x: oldPosition.x};
    }

    function calculateBottomElementPosition(oldPosition) {
        return {y: oldPosition.y + 1, x: oldPosition.x};
    }

    function isCellInputElementEmpty(element) {
        return !element.value;
    }

    function isPositionOfConstantCell(position) {
        if (!isPositionValid(position)) {
            return false;
        }
        return _isPositionOfConstantCell(position);
    }

    function _isPositionOfConstantCell(position) {
        return presenter.crossword[position.y][position.x][0].includes('!');
    }

    function isPositionOfNotBlankCell(position) {
        if (!isPositionValid(position)) {
            return false;
        }
        return _isPositionOfNotBlankCell(position);
    }

    function _isPositionOfNotBlankCell(position) {
        return presenter.crossword[position.y][position.x][0] !== ' ';
    }

    function getCellInput(position) {
        if (!isPositionValid(position)) {
            return;
        }
        return presenter.$view.find(`.cell_row_${position.y}.cell_column_${position.x}`).find("input")[0];
    }

    function getElementInput(element) {
        return $(element).find("input")[0];
    }

    function isPositionValid(position) {
        return !!position
            &amp;&amp; position.y &gt;= 0 &amp;&amp; position.y &lt; presenter.rowCount
            &amp;&amp; position.x &gt;= 0 &amp;&amp; position.x &lt; presenter.columnCount;
    }

    presenter.isDirectionNotSet = function () {
        return currentDirection === DIRECTIONS.NOT_SET;
    }

    presenter.isHorizontalDirection = function () {
        return currentDirection === DIRECTIONS.HORIZONTAL;
    }

    presenter.isVerticalDirection = function () {
        return currentDirection === DIRECTIONS.VERTICAL;
    }

    presenter.isTabIndexDirection = function () {
        return currentDirection === DIRECTIONS.TAB_INDEX;
    }

    presenter.isNextVerticalAnswerDirection = function () {
        return currentDirection === DIRECTIONS.NEXT_VERTICAL_ANSWER;
    }

    presenter.resetDirection = function () {
        currentDirection = DIRECTIONS.NOT_SET;
    }

    presenter.setHorizontalDirection = function () {
        currentDirection = DIRECTIONS.HORIZONTAL;
    }

    presenter.setVerticalDirection = function () {
        currentDirection = DIRECTIONS.VERTICAL;
    }

    presenter.setNextVerticalAnswerDirection = function () {
        currentDirection = DIRECTIONS.NEXT_VERTICAL_ANSWER
    }

    presenter.setTabIndexDirection = function () {
        currentDirection = DIRECTIONS.TAB_INDEX;
    }

    function setCaretPosition(elem, caretPos) {
        var range;

        if (elem.createTextRange) {
            range = elem.createTextRange();
            range.move('character', caretPos);
            range.select();
        } else {
            elem.focus();
            if (elem.selectionStart !== undefined) {
                elem.setSelectionRange(caretPos, caretPos);
            }
        }
    }

    presenter.createGrid = function() {
        var wordNumberCounter = 1;

        var gridContainer = $('&lt;div&gt;&lt;/div&gt;');
        addClass(gridContainer, "crossword_container");
        let containerWidth = presenter.columnCount * presenter.cellWidth;
        let containerHeight = presenter.rowCount * presenter.cellHeight;
        let containerMarginLeft = Math.round(presenter.columnCount * presenter.cellWidth / 2);
        let containerMarginTop = Math.round(presenter.rowCount * presenter.cellHeight / 2);
        if (isInPrintableStateMode()) {
            gridContainer.css({
                width: containerWidth + 'px',
                height: containerHeight + 'px',
            });
        } else {
            gridContainer.css({
                width: containerWidth + 'px',
                height: containerHeight + 'px',
                marginLeft: -1 * containerMarginLeft + 'px',
                marginTop: -1 * containerMarginTop + 'px'
            });
        }

        var tabIndexOffset = 0;
        for(var i = 0; i &lt; presenter.rowCount; i++) {
            for(var j = 0; j &lt; presenter.columnCount; j++) {
                var cellContainer = $('&lt;div&gt;&lt;/div&gt;');
                addClass(cellContainer, "cell_container");
                cellContainer.css({ width:  presenter.cellWidth + 'px',
                    height: presenter.cellHeight + 'px' });

                var cell = $('&lt;div&gt;&lt;/div&gt;');
                addClass(cell, "cell");
                addClass(cell, 'cell_' + i + 'x' + j);
                addClass(cell, 'cell_row_' + i);
                addClass(cell, 'cell_column_' + j);

                if (presenter.markedRowIndex &gt; 0 &amp;&amp; presenter.markedRowIndex == i+1) {
                    addClass(cell, "cell_row_marked");
                }

                if (presenter.markedColumnIndex &gt; 0 &amp;&amp; presenter.markedColumnIndex == j+1) {
                    addClass(cell, "cell_column_marked");
                }

                cellContainer.append(cell);
                if (presenter.crossword[i][j] == ' ') {
                    addClass(cell, "cell_blank");
                    addClass(cellContainer, "cell_container_blank");
                } else {
                    addClass(cell, "cell_letter");
                    addClass(cellContainer, "cell_container_letter");

                    var input = $('&lt;input type="text" maxlength="2" size="1"/&gt;');

                    if (presenter.crossword[i][j][0] === '!') {
                        input
                            .attr('tabIndex', presenter.tabIndexBase + tabIndexOffset++)
                            .focus(presenter.onCellInputFocus)
                            .val(presenter.crossword[i][j][1])
                            .prop('disabled', true);

                        addClass(cell, "cell_constant_letter");
                        cell.addClass("");
                    } else if (!isInPrintableStateMode()){
                        input
                            .attr('tabIndex', presenter.tabIndexBase + tabIndexOffset++)
                            .keyup(presenter.onCellInputKeyUp)
                            .keydown(presenter.onCellInputKeyDown)
                            .focus(presenter.onCellInputFocus)
                            .mouseup(presenter.onCellInputMouseUp)
                            .focusout(presenter.onCellInputFocusOut)
                            .click(presenter.onCellClick);
                    } else {
                        input
                            .prop('disabled', true)
                            .attr('tabIndex', presenter.tabIndexBase + tabIndexOffset++);
                    }

                    if(presenter.preview) {
                        input.attr({
                            value: capitalize(presenter.crossword[i][j]),
                            disabled: true
                        });
                    }

                    cell.append(input);

                    var horizontalWordBegin = presenter.isHorizontalWordBegin(i, j);
                    var verticalWordBegin = presenter.isVerticalWordBegin(i, j);

                    if (horizontalWordBegin) presenter.maxScore++;
                    if (verticalWordBegin) presenter.maxScore++;

                    if (horizontalWordBegin || verticalWordBegin) {
                        addClass(cell, "cell_word_begin");

                        if (horizontalWordBegin)
                            addClass(cell, "cell_word_begin_horizontal");

                        if (verticalWordBegin)
                            addClass(cell, "cell_word_begin_vertical");

                        if (!presenter.disableAutomaticWordNumbering) {
                            const sanitizedText = window.xssUtils.sanitize(wordNumberCounter++);
                            const wordNumber = $('&lt;div&gt;&lt;/div&gt;').html(sanitizedText);
                            addClass(wordNumber, "word_number");

                            cell.append(wordNumber);
                        }
                    }
                }

                presenter.maxTabIndex = presenter.tabIndexBase + tabIndexOffset;

                // Cell borders
                var borderStyle;
                var borderWidth;
                var borderColor;

                if(presenter.crossword[i][j] != ' ') {
                    borderStyle = presenter.letterCellsBorderStyle;
                    borderWidth = presenter.letterCellsBorderWidth;
                    borderColor = presenter.letterCellsBorderColor;

                } else {
                    borderStyle = presenter.blankCellsBorderStyle;
                    borderWidth = presenter.blankCellsBorderWidth;
                    borderColor = presenter.blankCellsBorderColor;
                }

                if(i === 0 || presenter.crossword[i-1][j] == ' ') { // Outer top border
                    cell.css({ borderTopStyle: borderStyle,
                        borderTopWidth: (borderWidth * 2) + 'px',
                        borderTopColor: borderColor,
                        top:            (borderWidth * -1) + 'px' });
                } else { // Inner top border
                    cell.css({ borderTopStyle: borderStyle,
                        borderTopWidth: borderWidth + 'px',
                        borderTopColor: borderColor });
                }

                if(i === presenter.rowCount - 1 || presenter.crossword[i+1][j] == ' ') { // Outer bottom border
                    cell.css({ borderBottomStyle: borderStyle,
                        borderBottomWidth: (borderWidth * 2) + 'px',
                        borderBottomColor: borderColor,
                        bottom:            (borderWidth * -1) + 'px' });
                } else { // Inner bottom border
                    cell.css({ borderBottomStyle: borderStyle,
                        borderBottomWidth: borderWidth + 'px',
                        borderBottomColor: borderColor });
                }

                if(j === 0 || presenter.crossword[i][j-1] == ' ') { // Outer left border
                    cell.css({ borderLeftStyle: borderStyle,
                        borderLeftWidth: (borderWidth * 2) + 'px',
                        borderLeftColor: borderColor,
                        left:            (borderWidth * -1) + 'px' });
                } else { // Inner left border
                    cell.css({ borderLeftStyle: borderStyle,
                        borderLeftWidth: borderWidth + 'px',
                        borderLeftColor: borderColor });
                }

                if(j === presenter.columnCount - 1 || presenter.crossword[i][j+1] == ' ') { // Outer right border
                    cell.css({ borderRightStyle: borderStyle,
                        borderRightWidth: (borderWidth * 2) + 'px',
                        borderRightColor: borderColor,
                        right:            (borderWidth * -1) + 'px' });
                } else { // Inner right border
                    cell.css({ borderRightStyle: borderStyle,
                        borderRightWidth: borderWidth + 'px',
                        borderRightColor: borderColor });
                }

                // Additional classes
                if(j == 0) {
                    addClass(cell, "cell_first_in_row");
                } else if(j == presenter.columnCount - 1) {
                    addClass(cell, "cell_last_in_row");
                }

                if(i == 0) {
                    addClass(cell, "cell_first_in_column");
                } else if(i == presenter.rowCount - 1) {
                    addClass(cell, "cell_last_in_column");
                }

                gridContainer.append(cellContainer);
            }
        }

        presenter.updateMaxScore();
        presenter.$view.append(gridContainer);
    };

    function returnErrorMessage(errorMessage, errorMessageSubstitutions) {
        return {
            isError: true,
            errorMessage: errorMessage,
            errorMessageSubstitutions: errorMessageSubstitutions
        }
    }

    presenter.updateMaxScore = function () {
        presenter.maxScore -= presenter.getNumberOfExampleWords();
    }

    presenter.getNumberOfExampleWords = function () {
        let exampleWordsCounter = 0;
        for (let i = 0; i &lt; presenter.rowCount; i++) {
            for (let j = 0; j &lt; presenter.columnCount; j++) {
                if (presenter.isHorizontalWordBegin(i, j)) {
                    let exampleLettersCounter = 0;
                    let letterCounter = 0;

                    for (let k = j; k &lt; presenter.columnCount; k++) {
                        if (presenter.crossword[i][k] === ' ') {
                            break;
                        }

                        if (presenter.crossword[i][k].match('![a-zA-Z]*?')) {
                            exampleLettersCounter++;
                        }
                        letterCounter++;
                    }

                    if (letterCounter === exampleLettersCounter) {
                        exampleWordsCounter++;
                    }
                }

                if (presenter.isVerticalWordBegin(i, j)) {
                    let exampleLettersCounter = 0;
                    let letterCounter = 0;

                    for (let k = i; k &lt; presenter.rowCount; k++) {
                        if (presenter.crossword[k][j] === ' ') {
                            break;
                        }

                        if (presenter.crossword[k][j].match('![a-zA-Z]*?')) {
                            exampleLettersCounter++;
                        }
                        letterCounter++;
                    }

                    if (letterCounter === exampleLettersCounter) {
                        exampleWordsCounter++;
                    }
                }
            }
        }

        return exampleWordsCounter;
    }

    presenter.readConfiguration = function(model) {
        if(typeof(model['Blank cells border color']) != "undefined" &amp;&amp; model['Blank cells border color'] !== "")
            presenter.blankCellsBorderColor = model['Blank cells border color'];

        if(typeof(model['Blank cells border width']) != "undefined" &amp;&amp; model['Blank cells border width'] !== "")
            presenter.blankCellsBorderWidth = parseInt(model['Blank cells border width']);

        if(typeof(model['Blank cells border style']) != "undefined" &amp;&amp; model['Blank cells border style'] !== "")
            presenter.blankCellsBorderStyle = model['Blank cells border style'];

        if(typeof(model['Letter cells border color']) != "undefined" &amp;&amp; model['Letter cells border color'] !== "")
            presenter.letterCellsBorderColor = model['Letter cells border color'];

        if(typeof(model['Letter cells border width']) != "undefined" &amp;&amp; model['Letter cells border width'] !== "")
            presenter.letterCellsBorderWidth = parseInt(model['Letter cells border width']);

        if(typeof(model['Letter cells border style']) != "undefined" &amp;&amp; model['Letter cells border style'] !== "")
            presenter.letterCellsBorderStyle = model['Letter cells border style'];

        if(typeof(model['Word numbers']) != "undefined") {
            if(model['Word numbers'] == "horizontal" || model['Word numbers'] == "both" || model['Word numbers'] === "")
                presenter.wordNumbersHorizontal = true;

            if(model['Word numbers'] == "vertical" || model['Word numbers'] == "both" || model['Word numbers'] === "")
                presenter.wordNumbersVertical = true;
        }

        if(typeof(model['Marked column index']) != "undefined" &amp;&amp; model['Marked column index'] !== "") {
            presenter.markedColumnIndex = parseInt(model['Marked column index']);
            if(presenter.markedColumnIndex &lt; 0) {
                return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_MARKED_COLUMN_INDEX)
            }
        }

        if(typeof(model['Marked row index']) != "undefined" &amp;&amp; model['Marked row index'] !== "") {
            presenter.markedRowIndex = parseInt(model['Marked row index']);
            if(presenter.markedRowIndex &lt; 0) {
                return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_MARKED_ROW_INDEX);
            }
        }

        presenter.disableAutomaticWordNumbering = model['Disable automatic word numberin'] == 'True';

        if(presenter.blankCellsBorderWidth &lt; 0) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_BLANK_CELLS_BORDER_WIDTH);
        }

        if(presenter.letterCellsBorderWidth &lt; 0) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_LETTER_CELLS_BORDER_WIDTH);
        }

        if(parseInt(model['Columns']) &lt;= 0 || isNaN(parseInt(model['Columns'])) ) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.COLUMNS_NOT_SPECIFIED);
        }

        if(parseInt(model['Rows']) &lt;= 0 || isNaN(parseInt(model['Rows']))) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.ROWS_NOT_SPECIFIED);
        }

        if(parseInt(model['Cell width']) &lt;= 0 || isNaN(parseInt(model['Cell width'])) ) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.CELL_WIDTH_NOT_SPECIFIED);
        }

        if(parseInt(model['Cell height']) &lt;= 0 || isNaN(parseInt(model['Cell height']))) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.CELL_HEIGHT_NOT_SPECIFIED);
        }

        presenter.rowCount        = parseInt(model['Rows']);
        presenter.columnCount     = parseInt(model['Columns']);
        presenter.cellWidth       = parseInt(model['Cell width']);
        presenter.cellHeight      = parseInt(model['Cell height']);

        var rows = model['Crossword'].split("\n");
        if(rows.length != presenter.rowCount) {
            return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_AMOUNT_OF_ROWS_IN_CROSSWORD);
        }

        for(var i = 0; i &lt; rows.length; i++) {
            if(rows[i].replace(/!/g, "").length != presenter.columnCount) {
                return returnErrorMessage(presenter.ERROR_MESSAGES.INVALID_AMOUNT_OF_COLUMNS_IN_CROSSWORD, { row : i + 1 });
            }

            var line = rows[i];
            var previous = line[0];

            if (line.slice(-1) === '!') {
                return returnErrorMessage(presenter.ERROR_MESSAGES.LAST_CHARACTER_EXCLAMATION_MARK);
            }

            for (var j=1; j&lt;line.length; j++) {
                if (previous === '!') {
                    switch (line[j]) {
                        case '!': return returnErrorMessage(presenter.ERROR_MESSAGES.DOUBLED_EXCLAMATION_MARK); break;
                        case ' ': return returnErrorMessage(presenter.ERROR_MESSAGES.EXCLAMATION_MARK_BEFORE_EMPTY_FIELD); break;
                        default: break;
                    }
                }
                previous = line[j];
            }
        }

        presenter.blockWrongAnswers = presenter.isBlockWrongAnswers(model);

        var autoNavigationPropertyResponse = readModelAutoNavigationMode(model);
        if (!!autoNavigationPropertyResponse) {
            return autoNavigationPropertyResponse;
        }

        return {
            isError: false,
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model['Is Visible']),
            langTag: model["langAttribute"],
        };
    };

    function readModelAutoNavigationMode(model) {
        const selectedMode = model["autoNavigation"];
        if (selectedMode === "Off") {
            autoNavigationMode = AUTO_NAVIGATION_OPTIONS.OFF;
        } else if (selectedMode === "Simple") {
            autoNavigationMode = AUTO_NAVIGATION_OPTIONS.SIMPLE;
        } else if (selectedMode === "Extended") {
            autoNavigationMode = AUTO_NAVIGATION_OPTIONS.EXTENDED;
        } else {
            return returnErrorMessage(
                presenter.ERROR_MESSAGES.NOT_SUPPORTED_SELECTED_AUTO_NAVIGATION_MODE
            );
        }
    }

    presenter.isAutoNavigationInOffMode = function () {
        return autoNavigationMode === AUTO_NAVIGATION_OPTIONS.OFF;
    }

    presenter.isAutoNavigationInSimpleMode = function () {
        return autoNavigationMode === AUTO_NAVIGATION_OPTIONS.SIMPLE;
    }

    presenter.isAutoNavigationInExtendedMode = function () {
        return autoNavigationMode === AUTO_NAVIGATION_OPTIONS.EXTENDED;
    }

    presenter.destroyCommands = function () {
        delete presenter.executeCommand;
        delete presenter.isAllOK;
        delete presenter.isAttempted;
        delete presenter.getMaxScore;
        delete presenter.getScore;
        delete presenter.getErrorCount;
        delete presenter.setShowErrorsMode;
        delete presenter.setWorkMode;
        delete presenter.show;
        delete presenter.hide;
        delete presenter.reset;
        delete presenter.getState;
        delete presenter.setState;
        presenter.isModelValid = false;
    };

    presenter.isBlockWrongAnswers = function (model) {
        return ModelValidationUtils.validateBoolean(model.blockWrongAnswers);
    };

    presenter.upgradeModel = function(model) {
        var upgradedModel = upgradeModelAddShowAllAnswersInGSAModeProperty(model);
        upgradedModel = upgradeModelAddAutoNavigationProperty(upgradedModel);
        upgradedModel = upgradeModelAddLangTagProperty(upgradedModel);
        return upgradeModelAddSpeechTextsProperty(upgradedModel);
    };

    function upgradeModelAddShowAllAnswersInGSAModeProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['showAllAnswersInGradualShowAnswersMode']) {
            upgradedModel['showAllAnswersInGradualShowAnswersMode'] = false;
        }

        return upgradedModel;
    }

    function upgradeModelAddAutoNavigationProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel["autoNavigation"]) {
            upgradedModel["autoNavigation"] = "Extended";
        }

        return upgradedModel;
    }

    function upgradeModelAddLangTagProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel["langAttribute"] === undefined) {
            upgradedModel["langAttribute"] =  '';
        }

        return upgradedModel;
    }

    function upgradeModelAddSpeechTextsProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Cell"]) {
            upgradedModel["speechTexts"]["Cell"] = {Cell: ""};
        }
        if (!upgradedModel["speechTexts"]["Across"]) {
            upgradedModel["speechTexts"]["Across"] = {Across: ""};
        }
        if (!upgradedModel["speechTexts"]["Down"]) {
            upgradedModel["speechTexts"]["Down"] = {Down: ""};
        }
        if (!upgradedModel["speechTexts"]["Correct"]) {
            upgradedModel["speechTexts"]["Correct"] = {Correct: ""};
        }
        if (!upgradedModel["speechTexts"]["Wrong"]) {
            upgradedModel["speechTexts"]["Wrong"] = {Wrong: ""};
        }
        if (!upgradedModel["speechTexts"]["Empty"]) {
            upgradedModel["speechTexts"]["Empty"] = {Empty: ""};
        }
        if (!upgradedModel["speechTexts"]["Disabled"]) {
            upgradedModel["speechTexts"]["Disabled"] = {Disabled: ""};
        }
        if (!upgradedModel["speechTexts"]["OutOf"]) {
            upgradedModel["speechTexts"]["OutOf"] = {OutOf: ""};
        }

        return upgradedModel;
    }

    presenter.initializeLogic = function(view, model) {
        model = presenter.upgradeModel(model);
        presenter.$view = $(view);
        presenter.ID = model.ID;
        presenter.showAllAnswersInGradualShowAnswersMode = model.showAllAnswersInGradualShowAnswersMode;

        presenter.configuration = presenter.readConfiguration(model);
        if(presenter.configuration.isError) {
            presenter.showErrorMessage(configuration.errorMessage, configuration.errorMessageSubstitutions);
            presenter.destroyCommands();
            return;
        }

        presenter.$view.find(".cell").live("blur", presenter.cellBlurEventHandler);
        presenter.prepareGrid(model);
        presenter.prepareCorrectAnswers();
        presenter.createGrid();
        presenter.setSpeechTexts(model["speechTexts"]);
        presenter.buildKeyboardController();
    };

    presenter.validate = function(mode) {
        const classPrefix = '.' + (isInPrintableStateMode() ? presenter.CSS_CLASSES.PRINTABLE + '_' : '');
        let wordValid, k, l, score, markedCell;
        let filled = false;

        if (presenter.isShowAnswersActive &amp;&amp; mode == presenter.VALIDATION_MODE.SHOW_ERRORS) {
            presenter.hideAnswers();
            for (let i = 0; i &lt; presenter.rowCount; i++) {
                for (let j = 0; j &lt; presenter.columnCount; j++) {
                    if (presenter.$view.find(classPrefix + 'cell_' + i + 'x' + j + ' input').val() != ''
                        &amp;&amp; typeof(presenter.$view.find(classPrefix + 'cell_' + i + 'x' + j + ' input').val()) !== "undefined"
                        &amp;&amp; presenter.crossword[i][j][0] !== '!') {
                        filled = true;
                    }
                }
            }
            if (!filled) {
                return;
            }
        }

        if(mode == presenter.VALIDATION_MODE.SHOW_ERRORS) {
            presenter.$view.find(classPrefix + "cell_letter input").attr('disabled', true);
        } else if(mode == presenter.VALIDATION_MODE.COUNT_SCORE) {
            score = 0;
        }

        for(i = 0; i &lt; presenter.rowCount; i++) {
            for(j = 0; j &lt; presenter.columnCount; j++) {
                if(presenter.isHorizontalWordBegin(i, j)) {
                    wordValid = true;
                    let exampleLettersCounter = 0;
                    let letterCounter = 0;

                    for(k = j; k &lt; presenter.columnCount; k++) {
                        if(presenter.crossword[i][k] == ' ') {
                            break;
                        }

                        if(presenter.crossword[i][k] != capitalize(presenter.$view.find(classPrefix + 'cell_' + i + 'x' + k + " input").attr('value'))
                            &amp;&amp; presenter.crossword[i][k][0] !== '!') {
                            wordValid = false;
                        }

                        if(presenter.crossword[i][k].match('![a-zA-Z]*?')) {
                            exampleLettersCounter++;
                        }
                        letterCounter++;
                    }

                    if (wordValid &amp;&amp; letterCounter === exampleLettersCounter &amp;&amp; mode !== presenter.VALIDATION_MODE.CHECK_ANSWERS) {
                        wordValid = false;
                    }

                    if(mode == presenter.VALIDATION_MODE.COUNT_SCORE &amp;&amp; wordValid) {
                        score++;
                    }

                    if(mode === presenter.VALIDATION_MODE.SHOW_ERRORS || mode === presenter.VALIDATION_MODE.CHECK_ANSWERS) {
                        for(l = j; l &lt; k; l++) {
                            markedCell = presenter.$view.find(classPrefix + 'cell_' + i + 'x' + l);
                            if(!markedCell.hasClass(presenter.CSS_CLASSES.CELL_VALID))
                                markedCell.addClass(
                                    wordValid
                                        ? presenter.CSS_CLASSES.CELL_VALID
                                        : presenter.CSS_CLASSES.CELL_INVALID);

                            if(wordValid &amp;&amp; markedCell.hasClass(presenter.CSS_CLASSES.CELL_INVALID))
                                markedCell.removeClass(presenter.CSS_CLASSES.CELL_INVALID);
                        }
                    }
                }

                if(presenter.isVerticalWordBegin(i, j)) {
                    wordValid = true;
                    let exampleLettersCounter = 0;
                    let letterCounter = 0;

                    for(k = i; k &lt; presenter.rowCount; k++) {
                        if(presenter.crossword[k][j] == ' ') {
                            break;
                        }

                        if(presenter.crossword[k][j] != capitalize(presenter.$view.find(classPrefix + 'cell_' + k + 'x' + j + " input").attr('value')) &amp;&amp; presenter.crossword[k][j][0] !== '!') {
                            wordValid = false;
                        }

                         if(presenter.crossword[k][j].match('![a-zA-Z]*?')) {
                            exampleLettersCounter++;
                        }
                        letterCounter++;
                    }

                    if (wordValid &amp;&amp; letterCounter === exampleLettersCounter &amp;&amp; mode !== presenter.VALIDATION_MODE.CHECK_ANSWERS) {
                        wordValid = false;
                    }

                    if(mode == presenter.VALIDATION_MODE.COUNT_SCORE &amp;&amp; wordValid) {
                        score++;
                    }

                    if(mode === presenter.VALIDATION_MODE.SHOW_ERRORS || mode === presenter.VALIDATION_MODE.CHECK_ANSWERS) {
                        for(l = i; l &lt; k; l++) {
                            markedCell = presenter.$view.find(classPrefix + 'cell_' + l + 'x' + j);
                            if(!markedCell.hasClass(presenter.CSS_CLASSES.CELL_VALID))
                                markedCell.addClass(
                                    wordValid
                                        ? presenter.CSS_CLASSES.CELL_VALID
                                        : presenter.CSS_CLASSES.CELL_INVALID);

                            if(wordValid &amp;&amp; markedCell.hasClass(presenter.CSS_CLASSES.CELL_INVALID))
                                markedCell.removeClass(presenter.CSS_CLASSES.CELL_INVALID);
                        }
                    }
                }

            }
        }

        if(mode == presenter.VALIDATION_MODE.COUNT_SCORE) {
            return score;
        }
    };

    presenter.setShowErrorsMode = function() {
        if (!presenter.isAttempted()) {
            return;
        }

        presenter.validate(presenter.VALIDATION_MODE.CHECK_ANSWERS);
    };

    presenter.setWorkMode = function() {
        presenter.$view.find(".cell_letter:not(.cell_constant_letter) input").attr('disabled', false);
        presenter.$view.find('.' + presenter.CSS_CLASSES.CELL_VALID).removeClass(presenter.CSS_CLASSES.CELL_VALID);
        presenter.$view.find('.' + presenter.CSS_CLASSES.CELL_INVALID).removeClass(presenter.CSS_CLASSES.CELL_INVALID);
    };

    presenter.cellBlurEventHandler = function () {
        if (presenter.isAllOK()) {
            presenter.sendAllOKEvent();
        }
    };

    presenter.run = function(view, model) {
        presenter.preview = false;
        presenter.addonID = model.ID
        presenter.initializeLogic(view, model);
        if (!presenter.configuration.isError) {
            presenter.setVisibility(presenter.configuration.isVisibleByDefault);
       }
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;

        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            eventBus.addEventListener(events[i], this);
        }
    };

    presenter.createPreview = function(view, model) {
        presenter.preview = true;
        presenter.initializeLogic(view, model);
        if (!presenter.configuration.isError) {
            presenter.setVisibility(true);
        }
    };

    presenter.reset = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        resetCellsStates()
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        presenter.setWorkMode();
    };

    function resetCellsStates() {
        for(var i = 0; i &lt; presenter.rowCount; i++) {
            for(var j = 0; j &lt; presenter.columnCount; j++) {
                if(presenter.crossword[i][j][0] !== '!') {
                    presenter.$view.find('.cell_' + i + 'x' + j + ' input').val('');
                }
                if(typeof(presenter.userAnswers) !== "undefined") {
                    presenter.userAnswers[i][j] = '';
                }
             }
        }
    }

    presenter.setVisibility = function(isVisible) {
        presenter.isVisible = isVisible;
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function() {
        if(presenter.isShowAnswersActive === true){
            presenter.hideAnswers();
        }
        presenter.setVisibility(true);
    };

    presenter.hide = function() {
        if(presenter.isShowAnswersActive === true){
            presenter.hideAnswers();
        }
        presenter.setVisibility(false);
    };

    presenter.isAttempted = function() {
        let classPrefix = "cell";
        if (isInPrintableStateMode()) {
            classPrefix = presenter.CSS_CLASSES.PRINTABLE + "_" + classPrefix;
        }

        var countedConstantLetters = 0;
        jQuery.each(presenter.$view.find(`.${classPrefix} input`), function() {
            if (!ModelValidationUtils.isStringEmpty($(this).val())) countedConstantLetters++;
        });

        return presenter.numberOfConstantLetters &lt; countedConstantLetters;
    };

    presenter.getScore = function() {
        const restoreState = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;

        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            presenter.hideAnswers();
        }
        var score = presenter.validate(presenter.VALIDATION_MODE.COUNT_SCORE);
        var finalScore = presenter.isAttempted() ? score : 0;
        if (restoreState) {
            presenter.showAnswers();
        }

        if (wasGradualShowAnswersActive) {
            this.restoreGradualShowAnswers();
        }
        return finalScore;
    };

    presenter.getMaxScore = function() {
        return presenter.maxScore;
    };

    presenter.getErrorCount = function() {
        const restoreState = presenter.isShowAnswersActive;
        const wasGradualShowAnswersActive = presenter.isGradualShowAnswersActive;
        if (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive) {
            presenter.hideAnswers();
        }
        var score = presenter.validate(presenter.VALIDATION_MODE.COUNT_SCORE),
            errorCount = presenter.getMaxScore() - score;
        var finalErrorCount = presenter.isAttempted() ? errorCount : 0;
        if (restoreState) {
            presenter.showAnswers();
        }

        if (wasGradualShowAnswersActive) {
            this.restoreGradualShowAnswers();
        }
        return finalErrorCount
    };

    presenter.getState = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        var cells = getCellsStates();
        var isVisible = presenter.isVisible;

        return JSON.stringify({
            cells : cells,
            isVisible : isVisible
        });
    };

    function getCellsStates() {
        var s = [];
        var cell;
        for(var i = 0; i &lt; presenter.rowCount; i++) {
            for(var j = 0; j &lt; presenter.columnCount; j++) {
                cell = presenter.$view.find('.cell_' + i + 'x' + j + ' input').attr('value');
                if(typeof(cell) == "string")
                    cell = cell.replace("\"", "\\\"");

                s.push(cell);
            }
        }
        return s;
    }

    presenter.setState = function(state) {
        var parsedState = $.parseJSON(state.toString());
        if (parsedState.hasOwnProperty("cells")) {
            setCellsStates(parsedState.cells);
        } else {
            setCellsStates(parsedState);
        }

        if (parsedState.hasOwnProperty("isVisible")) {
            if (typeof(parsedState.isVisible) === "boolean") {
                presenter.isVisible = parsedState.isVisible;
            } else {
                presenter.isVisible = presenter.configuration.isVisibleByDefault;
            }
            presenter.setVisibility(presenter.isVisible);
        }
    };

    function setCellsStates(cellsStates) {
        var counter = 0;
        for(var i = 0; i &lt; presenter.rowCount; i++) {
            for(var j = 0; j &lt; presenter.columnCount; j++) {
                presenter.$view.find('.cell_' + i + 'x' + j + ' input').attr('value', cellsStates[counter]);
                counter++;
            }
        }
    }

    presenter.setPlayerController = function (controller) {
        playerController = controller;
    };

    /**
     * @param controller (PrintableController)
     */
    presenter.setPrintableController = function (controller) {
        presenter.printableController = controller;
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'isAllOK': presenter.isAllOK,
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.isAllOK = function() {
        if (presenter.isWordNumbersCorrect()) {
            return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
        }
        return false;
    };

    function getEventObject(it, val, sc) {
        return {
            'source': presenter.ID,
            'item': '' + it,
            'value': '' + val,
            'score': '' + sc
        };
    }

    presenter.sendAllOKEvent = function () {
        eventBus.sendEvent('ValueChanged', getEventObject('all', '', ''));
    };

    presenter.sendCorrectWordEvent = function sendCorrectWordEvent (word, item) {
        eventBus.sendEvent('CorrectWord', getEventObject(item, word, ''));
    };

    presenter.sendScoreEvent = function(pos, value, isOk) {
        var item = '[row][col]'.replace('col', pos.x + 1).replace('row', pos.y + 1);
        var score = isOk ? '1' : '0';
        eventBus.sendEvent('ValueChanged', getEventObject(item, value, score));
    };

    presenter.validateWord = function validateWord(pos) {
        var max_x = pos.x;
        var max_y = pos.y;
        var i, k, horizontalResult = {
            word: '',
            item: 0,
            start: '',
            end: '',
        };
        var verticalResult = {...horizontalResult};

        if (presenter.wordNumbersHorizontal) {
            for (i = 0; i &lt;= max_x; i++) {
                if (!presenter.isHorizontalWordBegin(max_y, i)) {
                    continue;
                }
                for (k = i; k &lt; presenter.columnCount; k++) {
                    if(presenter.crossword[max_y][k] === ' ') {
                        break;
                    }
                    if(presenter.crossword[max_y][k] !== capitalize(presenter.$view.find('.cell_' + max_y + 'x' + k + " input").attr('value')) &amp;&amp; presenter.crossword[max_y][k][0] !== '!') {
                        horizontalResult.word = '';
                        break;
                    }
                    if (!horizontalResult.start) {
                        horizontalResult.start = `${k}:${max_y}`;
                    }
                    horizontalResult.end = `${k}:${max_y}`;
                    horizontalResult.word += presenter.crossword[max_y][k];
                }
                horizontalResult.item = presenter.$view.find('.cell_' + max_y + 'x' + i +' .word_number').text();
                break;
            }
        }

        if (presenter.wordNumbersVertical) {
            for (i = 0; i &lt;= max_y; i++) {
                if (!presenter.isVerticalWordBegin(i, max_x)) {
                    continue;
                }
                for (k = i; k &lt; presenter.rowCount; k++) {
                    if(presenter.crossword[k][max_x] === ' ') {
                        break;
                    }
                    if(presenter.crossword[k][max_x] !== capitalize(presenter.$view.find('.cell_' + k + 'x' + max_x + " input").attr('value')) &amp;&amp; presenter.crossword[k][max_x][0] !== '!') {
                        verticalResult.word = '';
                        break;
                    }
                    if (!verticalResult.start) {
                        verticalResult.start = `${max_x}:${k}`;
                    }
                    verticalResult.end = `${max_x}:${k}`;
                    verticalResult.word += presenter.crossword[k][max_x];
                }
                verticalResult.item = presenter.$view.find('.cell_' + i + 'x' + max_x +' .word_number').text();
                break;
            }
        }

        return {
            verticalResult: verticalResult.word !== '' ? verticalResult : null,
            horizontalResult: horizontalResult.word !== '' ? horizontalResult : null,
        }
    };

    presenter.getActivitiesCount = function() {
        if(presenter.showAllAnswersInGradualShowAnswersMode === "True") {
            return 1;
        }
        return presenter.correctAnswers.length;
    }

    presenter.onEventReceived = function (eventName, data) {
        if (!presenter.isModelValid) return;

        switch(eventName) {
            case "ShowAnswers":
                presenter.showAnswers();
                break;

            case "GradualShowAnswers":
                presenter.gradualShowAnswers(data);
                break;

            case "HideAnswers":
            case "GradualHideAnswers":
                presenter.hideAnswers();
                break;
        }
    }

    presenter.showAnswers = function () {
        if (presenter.isWordNumbersCorrect()) {
            presenter.saveUserAnswers();
            if (presenter.isShowAnswersActive) {
                presenter.hideAnswers();
            }
            presenter.isShowAnswersActive = true;
            presenter.setWorkMode();
            presenter.$view.find(".cell_letter input").addClass('crossword_cell_show-answers');

            presenter.replaceUserAnswersByModelAnswers();
        }
    };

    presenter.hideAnswers = function () {
        if (presenter.isWordNumbersCorrect() &amp;&amp; (presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive)) {
            presenter.$view.find(".cell_letter input").removeClass('crossword_cell_show-answers');
            presenter.isShowAnswersActive = false;
            presenter.isGradualShowAnswersActive = false;
            if (!presenter.userAnswers) {
                presenter.areUserAnswersSaved = false;
                return;
            }

            presenter.replaceAnswersBySavedUserAnswers();
        }
    };

    presenter.gradualShowAnswers = function (data) {
        if (data.moduleID !== presenter.addonID) {
            return;
        }

        if (!presenter.isWordNumbersCorrect()) {
            return;
        }

        presenter.isGradualShowAnswersActive = true;
        if (presenter.showAllAnswersInGradualShowAnswersMode === "True") {
            presenter.showAnswers();
        } else {
            presenter.$view.find(".cell_letter input").addClass('crossword_cell_show-answers');
            const itemIndex = parseInt(data.item, 10);
            presenter.GSAcounter = itemIndex;
            const answerData = presenter.correctAnswers[itemIndex];

            if (itemIndex === 0) presenter.prepareCrosswordForGSA();
            presenter.fillRowGaps(answerData);
        }
    }

    presenter.restoreGradualShowAnswers = function () {
        presenter.isGradualShowAnswersActive = true;
        if (presenter.showAllAnswersInGradualShowAnswersMode === "True") {
            presenter.showAnswers();
        } else {
            for (let i = 0; i &lt;= presenter.GSAcounter; i++) {
                presenter.$view.find(".cell_letter input").addClass('crossword_cell_show-answers');
                const answerData = presenter.correctAnswers[i];

                if (i === 0) presenter.prepareCrosswordForGSA();
                presenter.fillRowGaps(answerData);
            }
        }
    }

    presenter.saveUserAnswers = function () {
        if (presenter.areUserAnswersSaved) return;

        presenter.areUserAnswersSaved = true;
        presenter.$view.find(".cell_letter input:enabled").attr('disabled', true);
        presenter.userAnswers = new Array(presenter.rowCount);
        for (let row = 0; row &lt; presenter.rowCount; row++) {
            presenter.userAnswers[row] = new Array(presenter.columnCount);
            for (let column = 0; column &lt; presenter.columnCount; column++) {
                presenter.userAnswers[row][column] = presenter.$view.find('.cell_' + row + 'x' + column + ' input')
                    .val();
            }
        }
    }

    presenter.replaceUserAnswersByModelAnswers = function () {
        let classPrefix = "cell_";
        if (isInPrintableStateMode()) {
            classPrefix = presenter.CSS_CLASSES.PRINTABLE + "_" + classPrefix;
        }

        for (let row = 0; row &lt; presenter.rowCount; row++) {
            for (let column = 0; column &lt; presenter.columnCount; column++) {
                let $cellInputs = presenter.$view.find(`.${classPrefix}${row}x${column} input`);
                $cellInputs.val(presenter.crossword[row][column].replaceAll("!",""));
            }
        }
    }

    presenter.replaceAnswersBySavedUserAnswers = function () {
        let classPrefix = "cell_";
        if (isInPrintableStateMode()) {
            classPrefix = presenter.CSS_CLASSES.PRINTABLE + "_" + classPrefix;
        }

        for (let row = 0; row &lt; presenter.rowCount; row++) {
            for (let column = 0; column &lt; presenter.columnCount; column++) {
                if (!presenter.crossword[row][column][0].includes('!')) {
                    let $cellInputs = presenter.$view.find(`.${classPrefix}${row}x${column} input`);
                    $cellInputs.val(presenter.userAnswers[row][column]);
                    if (!isInPrintableStateMode()) {
                        $cellInputs.attr('disabled', false);
                    }
                }
            }
        }
        presenter.areUserAnswersSaved = false;
    }

    presenter.fillRowGaps = function (answerData) {
        const answer = answerData.answer.replaceAll("!","");
        const { x, y } = answerData.position;

        if (answerData.isHorizontal) {
            for (let i = 0; i &lt; answer.length; i++) {
                presenter.$view.find('.cell_' + y + 'x' + (x + i) + ' input').val(answer.charAt(i));
            }
        } else {
            for (let i = 0; i &lt; answer.length; i++) {
                presenter.$view.find('.cell_' + (y + i) + 'x' + x + ' input').val(answer.charAt(i));
            }
        }
    }

    presenter.prepareCrosswordForGSA = function () {
        presenter.setWorkMode();
        presenter.saveUserAnswers();
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            Cell: presenter.DEFAULT_TTS_PHRASES.CELL,
            Across: presenter.DEFAULT_TTS_PHRASES.ACROSS,
            Down: presenter.DEFAULT_TTS_PHRASES.DOWN,
            Correct: presenter.DEFAULT_TTS_PHRASES.CORRECT,
            Wrong: presenter.DEFAULT_TTS_PHRASES.WRONG,
            Empty: presenter.DEFAULT_TTS_PHRASES.EMPTY,
            Disabled: presenter.DEFAULT_TTS_PHRASES.DISABLED,
            OutOf: presenter.DEFAULT_TTS_PHRASES.OUT_OFF,
        };

        if (!speechTexts || $.isEmptyObject(speechTexts)) {
            return;
        }

        presenter.speechTexts = {
            Cell: TTSUtils.getSpeechTextProperty(
                speechTexts.Cell.Cell,
                presenter.speechTexts.Cell),
            Across: TTSUtils.getSpeechTextProperty(
                speechTexts.Across.Across,
                presenter.speechTexts.Across),
            Down: TTSUtils.getSpeechTextProperty(
                speechTexts.Down.Down,
                presenter.speechTexts.Down),
            Correct: TTSUtils.getSpeechTextProperty(
                speechTexts.Correct.Correct,
                presenter.speechTexts.Correct),
            Wrong: TTSUtils.getSpeechTextProperty(
                speechTexts.Wrong.Wrong,
                presenter.speechTexts.Wrong),
            Empty: TTSUtils.getSpeechTextProperty(
                speechTexts.Empty.Empty,
                presenter.speechTexts.Empty),
            Disabled: TTSUtils.getSpeechTextProperty(
                speechTexts.Disabled.Disabled,
                presenter.speechTexts.Disabled),
            OutOf: TTSUtils.getSpeechTextProperty(
                speechTexts.OutOf.OutOf,
                presenter.speechTexts.OutOf),
        };
    };

    presenter.keyboardController = function(keycode, isShiftDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftDown, event);
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject
            = new CrosswordKeyboardController(
                presenter.getElementsForKeyboardNavigation(),
                presenter.columnCount
        );
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return presenter.$view.find(`.${presenter.CSS_CLASSES.CELL}`);
    };

    presenter.setWCAGStatus = function(isWCAGOn) {
        presenter.isWCAGOn = isWCAGOn;
    };

    function CrosswordKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
        this.updateMapping();
    }

    CrosswordKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    CrosswordKeyboardController.prototype.constructor = CrosswordKeyboardController;

    CrosswordKeyboardController.prototype.exitWCAGMode = function () {
        KeyboardController.prototype.exitWCAGMode.call(this);
        presenter.setWCAGStatus(false);
    };

    CrosswordKeyboardController.prototype.updateMapping = function () {
        this.mapping[presenter.SPECIAL_KEYS.BACKSPACE] = this.executeBackspace;
        this.mapping[presenter.SPECIAL_KEYS.TAB] = this.nextTabIndexElement;
        this.shiftKeysMapping[presenter.SPECIAL_KEYS.TAB] = this.previousTabIndexElement;
    };

    CrosswordKeyboardController.prototype.enter = function (event) {
        if (event) {
            event.preventDefault();
        }

        if (this.keyboardNavigationActive) {
            this.readCurrentElement();
            return;
        }
        this.keyboardNavigationActive = true;
        this.moveToFirstElement();
        this.readCurrentElementInShortForm();
    };

    CrosswordKeyboardController.prototype.moveToFirstElement = function () {
        let cellPosition;
        if (presenter.isWCAGOn) {
            cellPosition = presenter.correctAnswers[0]["position"];
        } else {
            cellPosition = getPositionOfCellInputElement($(presenter.getFirstEditableCell()));
        }
        if (!isPositionValid(cellPosition)) {
            return;
        }

        presenter.setTabIndexDirection();
        if (presenter.keyboardControllerObject.keyboardNavigationActive) {
            presenter.keyboardControllerObject.switchCellInputElement(cellPosition);
        } else {
            focusCellInputUsingPosition(cellPosition);
        }
    }

    /**
     Action when was called tab
     @method nextTabIndexElement
    */
    CrosswordKeyboardController.prototype.nextTabIndexElement = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()) {
            presenter.setTabIndexDirection();
        }
        moveToNextEditableTabIndexCellInput(event);
    };

    presenter.getFirstEditableCell = function () {
        let firstElement = findCellInputElement(presenter.tabIndexBase);
        let isDisabled = $(firstElement).prop('disabled');
        if (isDisabled) {
            let tabIndex = presenter.tabIndexBase
            while (isDisabled &amp;&amp; tabIndex &lt; presenter.maxTabIndex) {
                firstElement = findCellInputElement(++tabIndex);
                isDisabled = $(firstElement).prop('disabled');
            }
        }

        return firstElement
    }

    function moveToNextEditableTabIndexCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getNextTabIndexEditableCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    /**
     Action when was called shift + tab
     @method previousTabIndexElement
    */
    CrosswordKeyboardController.prototype.previousTabIndexElement = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()) {
            presenter.setTabIndexDirection();
        }
        moveToPreviousEditableTabIndexCellInput(event);
    };

    function moveToPreviousEditableTabIndexCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getPreviousTabIndexEditableCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    /**
     Action when was called right arrow
     @method nextElement
    */
    CrosswordKeyboardController.prototype.nextElement = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()
            || presenter.isAutoNavigationInSimpleMode()) {
            presenter.setHorizontalDirection();
        }
        moveToNextRightCellInput(event);
    };

    function moveToNextRightCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getNextRightCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    /**
     Action when was called left arrow
     @method previousElement
    */
    CrosswordKeyboardController.prototype.previousElement = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()
            || presenter.isAutoNavigationInSimpleMode()) {
            presenter.setHorizontalDirection();
        }
        moveToNextLeftCellInput(event);
    };

    function moveToNextLeftCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getNextLeftCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    /**
     Action when was called down arrow
     @method nextRow
    */
    CrosswordKeyboardController.prototype.nextRow = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()
            || presenter.isAutoNavigationInSimpleMode()) {
            presenter.setVerticalDirection();
        }
        moveToNextBottomCellInput(event);
    };

    function moveToNextBottomCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getNextBottomCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    /**
     Action when was called up arrow
     @method previousRow
    */
    CrosswordKeyboardController.prototype.previousRow = function (event) {
        if (presenter.isAutoNavigationInExtendedMode()
            || presenter.isAutoNavigationInSimpleMode()) {
            presenter.setVerticalDirection();
        }
        moveToNextTopCellInput(event);
    };

    function moveToNextTopCellInput(event, blurCurrentOnAbsenceOfNext = false) {
        _moveToCellInputWithGivenPosition(
            event,
            getNextTopCellPositionOfCellInput,
            blurCurrentOnAbsenceOfNext
        );
    }

    function _moveToCellInputWithGivenPosition(event, getNewPosition, blurCurrentOnAbsenceOfNext = false) {
        if (event) {
            event.preventDefault();
            event.stopPropagation();
        }

        const currentCellInput = presenter.keyboardControllerObject.keyboardNavigationActive
            ? presenter.keyboardControllerObject.getCurrentInputElement()
            : event.target;
        const includeConstantCells = presenter.isWCAGOn;
        const nextCellPosition = getNewPosition(currentCellInput, includeConstantCells);
        if (!nextCellPosition) {
            if (blurCurrentOnAbsenceOfNext) {
                blurCellInput(currentCellInput);
            }
            return;
        }

        if (presenter.keyboardControllerObject.keyboardNavigationActive) {
            presenter.keyboardControllerObject.switchCellInputElement(nextCellPosition);
            presenter.keyboardControllerObject.readCurrentElementInShortForm();
        } else {
            focusCellInputUsingPosition(nextCellPosition);
        }
    }

    /**
     Action when was called backspace
     @method executeBackspace
    */
    CrosswordKeyboardController.prototype.executeBackspace = function (event) {
        handleBackspaceEvent(event);
    };

    function handleBackspaceEvent(event) {
        if (event) {
            event.stopPropagation();
        }

        var currentCellInput = presenter.keyboardControllerObject.keyboardNavigationActive
            ? presenter.keyboardControllerObject.getCurrentInputElement()
            : event.target;

        const hasValue = $(currentCellInput).val();
        const isDisabled = $(currentCellInput).prop('disabled');

        if (hasValue &amp;&amp; isDisabled || !hasValue) {
            var nextCellPosition = getNextCellPositionForBackspaceAction(currentCellInput);
            if (!nextCellPosition) {
                return;
            }

            if (presenter.keyboardControllerObject.keyboardNavigationActive) {
                presenter.keyboardControllerObject.switchCellInputElement(nextCellPosition);
                if (!isPositionOfConstantCell(nextCellPosition)) {
                    $(presenter.keyboardControllerObject.getCurrentInputElement()).val('');
                }
                presenter.keyboardControllerObject.readCurrentElementInShortForm();
            } else {
                var $previousElement = $(getCellInput(nextCellPosition));
                $previousElement.focus();
                $previousElement.val('');
            }
        }
    }

    function getNextCellPositionForBackspaceAction(currentCellInput) {
        if (presenter.isTabIndexDirection() || presenter.isDirectionNotSet()) {
            const previousTabIndex = currentCellInput.tabIndex - 1;
            return getPositionOfCellInputElementWithTabIndex(previousTabIndex, true);
        }

        const currentCellPosition = getPositionOfCellInputElement($(currentCellInput));
        if (presenter.isVerticalDirection()) {
            return getNextTopCellPosition(currentCellPosition, presenter.isWCAGOn);
        }
        return getNextLeftCellPosition(currentCellPosition, presenter.isWCAGOn);
    }

    CrosswordKeyboardController.prototype.switchCellInputElement = function (newPosition) {
        var newPositionIndex = this.calculateNewPositionIndex(newPosition);
        this.markCurrentElement(newPositionIndex);
    };

    CrosswordKeyboardController.prototype.calculateNewPositionIndex = function (newPosition) {
        var newPositionIndex = this.keyboardNavigationElements.toArray().findIndex(
            cellElement =&gt;
                $(cellElement).hasClass(`cell_row_${newPosition.y}`)
                &amp;&amp; $(cellElement).hasClass(`cell_column_${newPosition.x}`)
        );

        if (newPositionIndex === -1) {
            newPositionIndex = this.keyboardNavigationCurrentElementIndex;
        } else if (newPositionIndex &gt;= this.keyboardNavigationElementsLen) {
            newPositionIndex = this.keyboardNavigationElementsLen;
        } else if (newPositionIndex &lt; 0) {
            newPositionIndex = this.keyboardNavigationElementsLen;
        }
        return newPositionIndex;
    }

    CrosswordKeyboardController.prototype.mark = function (element) {
        KeyboardController.prototype.mark.call(this, element);
        var inputCell = getElementInput(element);
        $(inputCell).focus();
    };

    CrosswordKeyboardController.prototype.unmark = function (element) {
        KeyboardController.prototype.unmark.call(this, element);
        var inputCell = getElementInput(element);
        $(inputCell).blur();
    };

    CrosswordKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    }

    CrosswordKeyboardController.prototype.getCurrentElementPosition = function () {
        return presenter.getPosition(this.getCurrentElement());
    }

    CrosswordKeyboardController.prototype.getCurrentElement = function () {
        return this.getTarget(this.keyboardNavigationCurrentElement, false);
    };

    CrosswordKeyboardController.prototype.getCurrentInputElement = function () {
        return $(this.getTarget(this.keyboardNavigationCurrentElement, false)).find("input")[0];
    };

    CrosswordKeyboardController.prototype.readCurrentElement = function () {
        var textVoiceObject = [];
        var cellInput = this.getCurrentInputElement();
        var currentPosition = getPositionOfCellInputElement($(cellInput));

        this.addMessageAboutElementIndex(textVoiceObject, currentPosition);

        if (presenter.wordNumbersHorizontal) {
            this.addMessageAboutHorizontalAnswer(textVoiceObject, currentPosition);
        }

        if (presenter.wordNumbersVertical) {
            this.addMessageAboutVerticalAnswer(textVoiceObject, currentPosition);
        }

        this.addMessageAboutValue(textVoiceObject, cellInput.value);

        if (isPositionOfConstantCell(currentPosition)) {
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Disabled);
        } else {
            this.addMessageAboutCorrectness(textVoiceObject);
        }

        presenter.speak(textVoiceObject);
    };

    CrosswordKeyboardController.prototype.readCurrentElementInShortForm = function () {
        var textVoiceObject = [];
        var cellInput = this.getCurrentInputElement();
        var currentPosition = getPositionOfCellInputElement($(cellInput));

        this.addMessageAboutElementIndex(textVoiceObject, currentPosition);
        this.addMessageAboutValue(textVoiceObject, cellInput.value);

        if (isPositionOfConstantCell(currentPosition)) {
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Disabled);
        } else {
            this.addMessageAboutCorrectness(textVoiceObject);
        }

        presenter.speak(textVoiceObject);
    }

    CrosswordKeyboardController.prototype.addMessageAboutElementIndex = function (textVoiceObject, position) {
        const alphabet = "ABCDEFGHIJKLMNOPRSTUWXYZ";
        const message = presenter.speechTexts.Cell
            + " " + alphabet[position.x % alphabet.length]
            + " " + (position.y + 1);
        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, message);
    }

    CrosswordKeyboardController.prototype.addMessageAboutHorizontalAnswer = function (textVoiceObject, position) {
        var foundAnswer, trueLength, nextAnswer;
        for (var i = 0; i &lt; presenter.correctAnswers.length; i++) {
            nextAnswer = presenter.correctAnswers[i];

            if (nextAnswer.isHorizontal) {
                trueLength = [...nextAnswer.answer].filter((x) =&gt; x !== '!').length;
                if (position.y === nextAnswer.position.y
                    &amp;&amp; nextAnswer.position.x &lt;= position.x
                    &amp;&amp; position.x &lt; nextAnswer.position.x + trueLength) {
                    foundAnswer = {...nextAnswer};
                    foundAnswer["id"] = i + 1;
                    foundAnswer["trueLength"] = trueLength;
                    break;
                }
            }
        }

        if (foundAnswer) {
            const answerIDMessage = `${presenter.speechTexts.Across} ${foundAnswer.id}`;
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, answerIDMessage);

            const relativePosition = position.x - foundAnswer.position.x + 1;
            const outOfMessage = `${relativePosition} ${presenter.speechTexts.OutOf} ${foundAnswer.trueLength}`;
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, outOfMessage);
        }
    }

    CrosswordKeyboardController.prototype.addMessageAboutVerticalAnswer = function (textVoiceObject, position) {
        var foundAnswer, trueLength, nextAnswer;
        for (var i = 0; i &lt; presenter.correctAnswers.length; i++) {
            nextAnswer = presenter.correctAnswers[i];

            if (!nextAnswer.isHorizontal) {
                trueLength = [...nextAnswer.answer].filter((x) =&gt; x !== '!').length;
                if (position.x === nextAnswer.position.x
                    &amp;&amp; nextAnswer.position.y &lt;= position.y
                    &amp;&amp; position.y &lt; nextAnswer.position.y + trueLength) {
                    foundAnswer = {...nextAnswer};
                    foundAnswer["id"] = i + 1;
                    foundAnswer["trueLength"] = trueLength;
                    break;
                }
            }
        }

        if (foundAnswer) {
            const answerIDMessage = `${presenter.speechTexts.Down} ${foundAnswer.id}`;
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, answerIDMessage);

            const relativePosition = position.y - foundAnswer.position.y + 1;
            const outOfMessage = `${relativePosition} ${presenter.speechTexts.OutOf} ${foundAnswer.trueLength}`;
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, outOfMessage);
        }
    }

    CrosswordKeyboardController.prototype.addMessageAboutValue = function (textVoiceObject, value) {
        if (value) {
            pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, value);
        } else {
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Empty);
        }
    }

    CrosswordKeyboardController.prototype.addMessageAboutCorrectness = function (textVoiceObject) {
        var $cell = $(this.getCurrentElement());
        if ($cell.hasClass(presenter.CSS_CLASSES.CELL_VALID)) {
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Correct);
        } else if ($cell.hasClass(presenter.CSS_CLASSES.CELL_INVALID)) {
            pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Wrong);
        }
    }

    CrosswordKeyboardController.prototype.speakWrong = function () {
        var textVoiceObject = [];

        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Wrong);

        presenter.speak(textVoiceObject);
    }

    CrosswordKeyboardController.prototype.speakDisabled = function () {
        var textVoiceObject = [];

        pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, presenter.speechTexts.Disabled);

        presenter.speak(textVoiceObject);
    }

    function pushMessageToTextVoiceObjectWithLanguageFromLesson(textVoiceObject, message) {
        pushMessageToTextVoiceObject(textVoiceObject, message, false);
    }

    function pushMessageToTextVoiceObjectWithLanguageFromPresenter(textVoiceObject, message) {
        pushMessageToTextVoiceObject(textVoiceObject, message, true);
    }

    function pushMessageToTextVoiceObject(textVoiceObject, message, usePresenterLangTag = false) {
        if (usePresenterLangTag)
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message, presenter.configuration.langTag));
        else
            textVoiceObject.push(window.TTSUtils.getTextVoiceObject(message));
    }

    presenter.speak = function (data) {
        var tts = presenter.getTextToSpeechOrNull(playerController);
        if (tts &amp;&amp; presenter.isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        chosePrintableStateMode(showAnswers);
        const snapshot = createSnapshotOfVariables();

        createPrintableBaseView(model);
        modifyViewAccordingToPrintableStateMode();
        const printableHTML = presenter.$view[0].outerHTML;

        restoreSnapshotOfVariables(snapshot);
        presenter.printableStateMode = null;

        return printableHTML;
    };

    function createPrintableBaseView(model) {
        model = presenter.upgradeModel(model);

        presenter.$view = $('&lt;div&gt;&lt;/div&gt;');
        presenter.$view.attr("id", model.ID);
        presenter.$view.addClass(presenter.CSS_CLASSES.PRINTABLE_ADDON);

        presenter.printableConfiguration = presenter.readConfiguration(model);
        if (presenter.printableConfiguration.isError) {
            presenter.showErrorMessage(
                presenter.printableConfiguration.errorMessage,
                presenter.printableConfiguration.errorMessageSubstitutions
            );
            presenter.destroyCommands();
            return;
        }

        presenter.prepareGrid(model);
        presenter.prepareCorrectAnswers();
        presenter.createGrid();
    }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        } else {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function isInPrintableStateMode() {
        return presenter.printableStateMode !== null;
    }

    function modifyViewAccordingToPrintableStateMode() {
        switch (presenter.printableStateMode) {
            case presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS:
                updateViewToCheckAnswersInPrintableStateMode();
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS:
                updateViewToShowUserAnswersInPrintableStateMode();
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS:
                updateViewToShowAnswersInPrintableStateMode();
                break;
            case presenter.PRINTABLE_STATE_MODE.EMPTY:
                updateViewToEmptyStateInPrintableStateMode();
                break;
        }
        replaceInputElementsByPrintableDivs();
    }

    function updateViewToCheckAnswersInPrintableStateMode() {
        setUserAnswersFromPrintableState();
        presenter.replaceAnswersBySavedUserAnswers();

        if (!presenter.isAttempted()) {
            return;
        }
        validateForPrintable();
    }

    function validateForPrintable() {
        presenter.validate(presenter.VALIDATION_MODE.SHOW_ERRORS);
        for (let row = 0; row &lt; presenter.rowCount; row++) {
            for (let column = 0; column &lt; presenter.columnCount; column++) {
                let $cell = presenter.$view.find(`.${presenter.CSS_CLASSES.PRINTABLE}_cell_${row}x${column}`);
                if ($cell.hasClass(presenter.CSS_CLASSES.CELL_VALID)) {
                    $cell.append(getPrintableGapSignHTML(true));
                    $cell.removeClass(presenter.CSS_CLASSES.CELL_VALID);
                    $cell.addClass(presenter.CSS_CLASSES.PRINTABLE_CELL_VALID);
                } else if ($cell.hasClass(presenter.CSS_CLASSES.CELL_INVALID)) {
                    $cell.append(getPrintableGapSignHTML(false));
                    $cell.removeClass(presenter.CSS_CLASSES.CELL_INVALID);
                    $cell.addClass(presenter.CSS_CLASSES.PRINTABLE_CELL_INVALID);
                }
            }
        }
    }

    function getPrintableGapSignHTML(isCorrectAnswer) {
        const $sign = $("&lt;span&gt;&lt;/span&gt;");
        if (isCorrectAnswer) {
            $sign.addClass(presenter.CSS_CLASSES.PRINTABLE_SIGN_VALID);
        } else {
            $sign.addClass(presenter.CSS_CLASSES.PRINTABLE_SIGN_INVALID);
        }
        return $sign;
    }

    function updateViewToShowUserAnswersInPrintableStateMode() {
        setUserAnswersFromPrintableState();
        presenter.replaceAnswersBySavedUserAnswers();
    }

    function setUserAnswersFromPrintableState() {
        presenter.userAnswers = [];

        let cells;
        if (presenter.printableState.hasOwnProperty("cells")) {
            cells = presenter.printableState.cells;
        } else {
            cells = presenter.printableState;
        }
        let counter = 0;
        for (let i = 0; i &lt; presenter.rowCount; i++) {
            let row = [];
            for (let j = 0; j &lt; presenter.columnCount; j++) {
                row.push(cells[counter]);
                counter++;
            }
            presenter.userAnswers.push(row);
        }
    }

    function updateViewToShowAnswersInPrintableStateMode() {
        presenter.replaceUserAnswersByModelAnswers();
    }

    function updateViewToEmptyStateInPrintableStateMode() {}

    function replaceInputElementsByPrintableDivs() {
        presenter.$view.find(".printable_cell_letter input").replaceWith(function() {
            let $cellContent = $('&lt;div&gt;' + $(this).val() + '&lt;/div&gt;');
            $cellContent.addClass(presenter.CSS_CLASSES.PRINTABLE_CELL_LETTER_CONTENT);
            return $cellContent
        });
    }

    function createSnapshotOfVariables() {
        return {
            crossword: presenter.crossword,
            rowCount: presenter.rowCount,
            columnCount: presenter.columnCount,
            cellHeight: presenter.cellHeight,
            cellWidth: presenter.cellWidth,
            blankCellsBorderStyle: presenter.blankCellsBorderStyle,
            blankCellsBorderWidth: presenter.blankCellsBorderWidth,
            blankCellsBorderColor: presenter.blankCellsBorderColor,
            letterCellsBorderStyle: presenter.letterCellsBorderStyle,
            letterCellsBorderWidth: presenter.letterCellsBorderWidth,
            letterCellsBorderColor: presenter.letterCellsBorderColor,
            wordNumbersHorizontal: presenter.wordNumbersHorizontal,
            wordNumbersVertical: presenter.wordNumbersVertical,
            showAllAnswersInGradualShowAnswersMode: presenter.showAllAnswersInGradualShowAnswersMode,
            disableAutomaticWordNumbering: presenter.disableAutomaticWordNumbering,
            blockWrongAnswers: presenter.blockWrongAnswers,
            isVisibleByDefault: presenter.isVisibleByDefault,
            markedColumnIndex: presenter.markedColumnIndex,
            markedRowIndex: presenter.markedRowIndex,
            autoNavigationMode: autoNavigationMode,

            $view: presenter.$view,
            tabIndexBase: presenter.tabIndexBase,
            maxTabIndex: presenter.maxTabIndex,
            numberOfConstantLetters: presenter.numberOfConstantLetters,
            maxScore: presenter.maxScore,
            areUserAnswersSaved: presenter.areUserAnswersSaved,
            configuration: {...presenter.configuration},
            correctAnswers: [...presenter.correctAnswers]
        }
    }

    function restoreSnapshotOfVariables(snapshot) {
        Object.keys(snapshot).forEach(function(key) {
            if (presenter.hasOwnProperty(key)) {
                presenter[key] = snapshot[key];
            } else {
                switch (key.toString()) {
                    case "autoNavigationMode":
                        autoNavigationMode = snapshot[key];
                        break;
                    default:
                        break;
                }
            }
        });
    }

    function addClass($element, className) {
        if (isInPrintableStateMode()) {
            className = presenter.CSS_CLASSES.PRINTABLE + "_" + className;
        }
        $element.addClass(className);
    }

    function capitalize(value) {
        // standard toUpperCase uses 'SS' as capital 'ß', which causes errors
        if (value === 'ß') return 'ß';
        return value.toUpperCase();
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="eKeyboard" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Work With" name="workWith" nameLabel="eKeyboard_property_work_with" type="text"/>
        <property displayName="Works With All" name="worksWithAll" nameLabel="eKeyboard_property_works_with_all" type="boolean"/>
        <property displayName="Layout Type" name="layoutType" nameLabel="eKeyboard_property_layout_type" type="{Numeric, French (Special Characters), German (Special Characters), Italian (Special Characters), Spanish (Special Characters), Custom}"/>
        <property displayName="Custom Layout" isLocalized="true" name="customLayout" nameLabel="eKeyboard_property_custom_layout" type="text"/>
        <property displayName="Position My" name="positionMy" nameLabel="eKeyboard_property_position_my" type="string"/>
        <property displayName="Position At" name="positionAt" nameLabel="eKeyboard_property_position_at" type="string"/>
        <property displayName="Max Characters" name="maxCharacters" nameLabel="eKeyboard_property_max_characters" type="string"/>
        <property displayName="Don't Open On Focus" name="noOpenOnFocus" nameLabel="eKeyboard_property_no_open_on_focus" type="boolean"/>
        <property displayName="Lock Standard Keyboard Input" name="lockStandardKeyboardInput" nameLabel="eKeyboard_property_lock_standard_keyboard_input" type="boolean"/>
        <property displayName="Custom Display" isLocalized="true" name="customDisplay" nameLabel="eKeyboard_property_custom_display" type="text"/>
        <property displayName="Show Close Button" name="showCloseButton" nameLabel="eKeyboard_property_show_close_button" type="boolean"/>
	</model>
<css>/* keyboard - jQuery UI Widget */
.ui-keyboard { padding: .3em; position: absolute; left: 0; top: 0; z-index: 16000; }
.ui-keyboard-has-focus { z-index: 16001; }
.ui-keyboard div { font-size: 1.1em; }
.ui-keyboard-button { height: 2em; width: 2em; min-width: 1em; margin: .1em; cursor: pointer; overflow: hidden; line-height: 2em; -moz-user-focus: ignore; }
.ui-keyboard-button span { padding: 0; margin: 0; white-space:nowrap; display: inline-block; }
.ui-keyboard-button-endrow { clear: left; }
.ui-keyboard-widekey { min-width: 4em; width: auto; }
.ui-keyboard-space { width: 15em; }
.ui-keyboard-space span, .ui-keyboard-empty span { font: 0/0 a; text-shadow: none; color: transparent; } /* see http://nicolasgallagher.com/another-css-image-replacement-technique/ */
.ui-keyboard-preview-wrapper { text-align: center; }
.ui-keyboard-preview { text-align: left; margin: 0 0 3px 0; display: inline; width: 99%;} /* width is calculated in IE, since 99% = 99% full browser width =( */
.ui-keyboard-keyset { text-align: center; white-space: nowrap; }
.ui-keyboard-input { text-align: left; }
.ui-keyboard-input-current { -moz-box-shadow: 1px 1px 10px #00f; -webkit-box-shadow: 1px 1px 10px #00f; box-shadow: 1px 1px 10px #00f; }
.ui-keyboard-placeholder { color: #888; }
.ui-keyboard-nokeyboard { color: #888; border-color: #888; } /* disabled or readonly inputs, or use input[disabled='disabled'] { color: #f00; } */
.ui-keyboard-button.disabled { opacity: 0.5; filter: alpha(opacity=50); } /* used by the acceptValid option to make the accept button appear faded */
.ui-keyboard-spacer { display: inline-block; width: 1px; height: 0; }

/* combo key styling - toggles diacritics on/off */
.ui-keyboard-button.ui-keyboard-combo.ui-state-default { border-color: #ffaf0f; }

/* (in)valid inputs */
button.ui-keyboard-accept.ui-keyboard-valid-input { border-color: #0c0; background: #080; color: #fff; }
button.ui-keyboard-accept.ui-keyboard-valid-input:hover { background: #0a0; }
button.ui-keyboard-accept.ui-keyboard-invalid-input { border-color: #c00; background: #800; color: #fff; }
button.ui-keyboard-accept.ui-keyboard-invalid-input:hover { background: #a00; }

/*** jQuery Mobile definitions ***/
/* jQuery Mobile styles - need wider buttons because of font size and text-overflow:ellipsis */
.ui-bar .ui-keyboard-button { width: 3em; display: inline-block; }
.ui-bar .ui-keyboard-widekey { width: 5.5em; }
.ui-bar .ui-keyboard-space { width: 15em; }
.ui-bar .ui-keyboard-space span { visibility: hidden; } /* hides the ellipsis */
.ui-bar .ui-keyboard-keyset { line-height: 0.5em; }
.ui-bar input.ui-input-text, .ui-bar textarea.ui-input-text { width: 95%; }

/* over-ride padding set by mobile ui theme - needed because the mobile script wraps button text with several more spans */
.ui-bar .ui-btn-inner { height: 2em; padding: 0.2em 0; margin: 0; }
.ui-bar .ui-btn { margin: 0; font-size: 13px; } /* mobile default size is 13px */

/* Media Queries (optimized for jQuery UI themes; may be slightly off in jQuery Mobile themes) */
/* 240 x 320 (small phone)  */
@media all and (max-width: 319px) {
    .ui-keyboard div { font-size: 9px; }
    .ui-keyboard .ui-keyboard-input { font-size: 12px; }
    /* I don't own an iPhone so I have no idea how small this really is... is it even clickable with your finger? */
    .ui-bar .ui-btn { margin: 0; font-size: 9px; }
    .ui-bar .ui-keyboard-button { width: 1.8em; height: 2.5em; }
    .ui-bar .ui-keyboard-widekey { width: 4em; }
    .ui-bar .ui-keyboard-space { width: 8em; }
    .ui-bar .ui-btn-inner { height: 2.5em; padding: 0.3em 0; }
}

/* 320 x 480 (iPhone)  */
@media all and (min-width: 320px) and (max-width: 479px) {
    .ui-keyboard div { font-size: 9px; }
    .ui-keyboard .ui-keyboard-input { font-size: 14px; }
    /* I don't own an iPhone so I have no idea how small this really is... is it even clickable with your finger? */
    .ui-bar .ui-btn { margin: 0; font-size: 11px; }
    .ui-bar .ui-keyboard-button { width: 1.8em; height: 3em; }
    .ui-bar .ui-keyboard-widekey { width: 4.5em; }
    .ui-bar .ui-keyboard-space { width: 10em; }
    .ui-bar .ui-btn-inner { height: 3em; padding: 0.7em 0; }
}

/* 480 x 640 (small tablet) */
@media all and (min-width: 480px) and (max-width: 767px) {
    .ui-keyboard div { font-size: 13px; }
    .ui-keyboard .ui-keyboard-input { font-size: 14px; }
    .ui-bar .ui-btn { margin: 0; font-size: 10px; }
    .ui-bar .ui-keyboard-button { height: 2.5em; }
    .ui-bar .ui-btn-inner { height: 2.5em; padding: 0.5em 0; }
}

.eKeyboard-close-button {
	border: 1px solid #cccccc;
  	background: #f6f6f6;
  	color: red;
    top: 0;
    left: 101%;
}

.eKeyboard-open-button {
  	width: 33px;
  	height: 33px;
    background-color: transparent;
    border: 0px;
    background-position: center;
    background-image: url("resources/eKeyboard.png");
    background-repeat: no-repeat;
    background-size: 32px 32px;
}


</css><view>&lt;div class="eKeyboard-wrapper"&gt;
    &lt;div class="eKeyboard-container"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="eKeyboard-wrapper"&gt;
    &lt;div class="eKeyboard-container"&gt;

    &lt;/div&gt;
&lt;/div&gt;
</preview><presenter>function AddoneKeyboard_create(){

    var presenter = function(){};

    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.display = null;
    presenter.isLoaded = false;
    presenter.functionsQueue = [];

    var keyboardIsVisible = true;
    var closeButtonElement = null;
    var openButtonElement = null;
    var lastClickedElement = null;
    var movedInput = false;
    var escClicked = false;

    presenter.ERROR_CODES = {
        'E01' : 'The position is wrong. See documentation for more details.',
        'E02' : 'Module ID not found.',
        'E03' : 'The module you provided has no getView method implemented.',
        'E04' : 'Max Characters must be a digit or empty string (unlimited).'
    };


    presenter.LAYOUT_TO_LANGUAGE_MAPPING = {
        'french (special characters)' : "{ \
            'default': ['\u00e0 \u00e2 \u00e7 \u00e8 \u00e9 \u00ea \u00eb \u00ee \u00ef \u00f4 \u00f9 \u00fb \u00e6 \u0153 \u00ab \u00bb {shift}'], \
            'shift':   ['\u00c0 \u00c2 \u00c7 \u00c8 \u00c9 \u00ca \u00cb \u00ce \u00cf \u00d4 \u00d9 \u00db \u00c6 \u0152 \u00ab \u00bb {shift}'] \
        }",
        'german (special characters)' : "{ \
            'default': ['\u00e4 \u00f6 \u00fc \u00df {shift}'], \
            'shift': ['\u00c4 \u00d6 \u00dc {empty} {shift}'] \
        }",
        'spanish (special characters)' : "{ \
            'default': ['\u00e1 \u00e9 \u00ed \u00f3 \u00fa \u00f1 \u00e7 \u00fc \u00a1 \u00bf \u00ba \u00aa {shift}'], \
            'shift': ['\u00c1 \u00c9 \u00cd \u00d3 \u00da \u00d1 \u00c7 \u00dc {empty} {empty} {empty} {empty} {shift}'] \
        }",
        'italian (special characters)' : "{ \
            'default': ['\u00e0 \u00e8 \u00e9 \u00ec \u00f2 \u00f9 {shift}'], \
            'shift': ['\u00c0 \u00c8 \u00c9 \u00cc \u00d2 \u00d9 {shift}'] \
        }"
    };

    function getErrorObject (ec) { return { isError: true, errorCode: ec }; }
    function getCorrectObject(val) { return { isError: false, value: val }; }

    function touchStartDecorator(func, element) {
        $(element).on('click', function (ev) {
            ev.preventDefault();
            ev.stopPropagation();

            func();
        });
    }

    presenter.initializeCloseButton = function AddoneKeyboard_initializeCloseButton() {
        closeButtonElement = document.createElement('button');
        closeButtonElement.className = 'eKeyboard-close-button';
        closeButtonElement.style.position = 'absolute';
        closeButtonElement.innerHTML = '&lt;span&gt;\u2716&lt;/span&gt;';
        closeButtonElement.style.display = 'none';

        $(presenter.keyboardWrapper).append(closeButtonElement);

        touchStartDecorator(closeButtonCallBack, closeButtonElement);
    };

    function initializeOpenButton() {
        openButtonElement = document.createElement('button');
        openButtonElement.className = 'eKeyboard-open-button';
        openButtonElement.style.display = 'none';
        openButtonElement.style.zindex = 'none';
        $(openButtonElement).on('mousedown', function (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            showOpenButtonCallback();
        });

        $("body").append(openButtonElement);
    }

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);
    };

    presenter.onEventReceived = function(eventName) {
        if (eventName == 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        }
    };

    presenter.validateType = function(rawType) {
        if (rawType == 'Numeric' || rawType.length == 0) {
            return 'num';
        }

        return rawType.toLowerCase();
    };

    presenter.validateWorkWith = function (workWithModules, worksWithAll) {
        const workWithViews = [];
        for (let i = 0; i &lt; workWithModules.length; i++) {
            const module = presenter.playerController.getModule(workWithModules[i]);
            if (module) {
                if (module.getView &amp;&amp; module.getView()) {
                    workWithViews.push(module.getView());
                }
            } else if (!worksWithAll) {
                return getErrorObject("E02");
            }
        }

        return workWithViews;
    }

    presenter.validateMaxCharacters = function(rawMaxCharacters) {
        if (rawMaxCharacters.length == 0) {
            return getCorrectObject(false);
        }

        if ( !(/\d+/.test(rawMaxCharacters)) ) {
            return getErrorObject("E04");
        }

        return getCorrectObject(parseInt(rawMaxCharacters, 10));
    };

    presenter.validatePosition = function(rawPosition, isMy) {
        if (!rawPosition.length) {
            return getCorrectObject(isMy ? 'left center' : 'right center');
        }

        var possibilitiesOnTheLeft = ['left', 'center', 'right'],
            possibilitiesOnTheRight = ['top', 'center', 'bottom'],
            splitted = rawPosition.split(' ');

        if (
            splitted.length === 2 &amp;&amp;
            possibilitiesOnTheLeft.indexOf(splitted[0]) &gt;= 0 &amp;&amp;
            possibilitiesOnTheRight.indexOf(splitted[1]) &gt;= 0
        ) {
            return getCorrectObject(rawPosition);
        } else {
            return getErrorObject("E01")
        }
    };

    presenter.validateOffsetData = function(positionMy, positionAt) {
        var splittedMy = positionMy.split(' '),
            splittedAt = positionAt.split(' ');

        if (splittedMy[1] == 'bottom' &amp;&amp; splittedAt[1] == 'top') {
            return {
                orientation: 'horizontal',
                directionSign: '-',
                value: '0 -10'
            }
        }

        if (splittedMy[0] == 'left' &amp;&amp; splittedAt[0] == 'right') {
            return {
                orientation: 'vertical',
                directionSign: '',
                value: '10 0'
            };
        }

        if (splittedMy[1] == 'top' &amp;&amp; splittedAt[1] == 'bottom') {
            return {
                orientation: 'horizontal',
                directionSign: '',
                value : '0 10'
            };
        }

        if (splittedMy[0] == 'right' &amp;&amp; splittedAt[0] == 'left') {
            return {
                orientation: 'vertical',
                directionSign: '-',
                value: '-10 0'
            };
        }

        return {
            orientation: 'none',
            directionSign: '',
            value : ''
        };
    };

    presenter.createPreview = function(view, model) {
        runLogic(view, model, true);
    };

    presenter.upgradeModel = function AddoneKeyboard_upgradeModel(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["worksWithAll"]) {
            upgradedModel["worksWithAll"] = "False";
        }
        if (!upgradedModel['customDisplay']) {
            upgradedModel['customDisplay'] = "";
        }

        return upgradedModel;
    }

    presenter.validateModel = function(model, isPreview) {
        const worksWithAll = ModelValidationUtils.validateBoolean(model.worksWithAll);
        let workWithModules = worksWithAll ? presenter.getAllPageModulesIds() : Helpers.splitLines(model['workWith']),
            workWithViews = [],
            layoutType = presenter.validateType(model['layoutType']),
            customLayout = model['customLayout'],
            maxCharacters = presenter.validateMaxCharacters(model['maxCharacters']),
            positionMy = presenter.validatePosition(model['positionMy'], true),
            positionAt = presenter.validatePosition(model['positionAt'], false);

        if (!isPreview) {
            workWithViews = presenter.validateWorkWith(workWithModules, worksWithAll);
        }

        if (presenter.LAYOUT_TO_LANGUAGE_MAPPING[layoutType] != undefined) {
            customLayout = presenter.LAYOUT_TO_LANGUAGE_MAPPING[layoutType];
            layoutType = 'custom';
        }

        if (maxCharacters.isError) return maxCharacters;
        if (positionMy.isError) return positionMy;
        if (positionAt.isError) return positionAt;
        if (workWithViews.isError) return workWithViews;

        return {
            'ID': model["ID"],
            'isError' : false,
            workWithViews,
            worksWithAll,
            layoutType,
            customLayout,
            positionAt,
            positionMy,
            'customDisplay' : model['customDisplay'],
            'maxCharacters' : maxCharacters.value,
            'offset' : presenter.validateOffsetData(positionMy.value, positionAt.value),
            'openOnFocus' : !ModelValidationUtils.validateBoolean(model['noOpenOnFocus']),
            'lockInput' : ModelValidationUtils.validateBoolean(model['lockStandardKeyboardInput']),
            'showCloseButton': ModelValidationUtils.validateBoolean(model['showCloseButton'])
        }
    };

    presenter.getAllPageModulesIds = function () {
        const currentPageIndex = presenter.playerController.getCurrentPageIndex();
        const allModulesIds = presenter.playerController.getPresentation().getPage(currentPageIndex).getModulesAsJS();

        return allModulesIds;
    }

    presenter.removeEventListeners = function () {
        presenter.configuration.$inputs.each(function (index, element) {
            element.removeEventListener('mousedown', presenter.focusOnMouseDown);
            element.removeEventListener('focus', presenter.openEKeyboardOnFocus);
            element.removeEventListener('forceClick', presenter.openEKeyboardOnForceClick);
            element.removeEventListener('keyup', presenter.onESCHideKeyboard);
            element.removeEventListener('change', presenter.moveToNextGap);
            element.removeEventListener('paste', presenter.moveToNextGap);
            element.removeEventListener('keyup', presenter.moveToNextGap);
            element.removeEventListener('focusout', focusoutCallBack);
        });
    };

    function runLogic(view, model, isPreview) {
        presenter.$view = $(view);
        presenter.view = view;
        presenter.isPreview = isPreview;
        presenter.isShowCloseButton = false;

        presenter.pageLoadedDeferred = new $.Deferred();
        presenter.pageLoaded = presenter.pageLoadedDeferred.promise();

        presenter.keyboardWrapper = document.createElement("div");
        presenter.keyboardWrapper.className = "ui-ekeyboard-wrapper";
        $(document.body).append(presenter.keyboardWrapper);

        initializeOpenButton();
        presenter.initializeCloseButton();

        MutationObserverService.createDestroyObserver(model["ID"], presenter.destroy, model["ID"]);
        MutationObserverService.setObserver();

        var mathJaxDeferred = new jQuery.Deferred(),
            mathJaxProcessEnded = mathJaxDeferred.promise();

        MathJax.Hub.Register.MessageHook("End Process", function (message) {
            if ($(message[1]).hasClass('ic_page')) {
                if(mathJaxDeferred.state() != 'resolved'){
                    mathJaxDeferred.resolve();
                }
            }

            if ($(message[1]).hasClass('ic_popup_page')) {
                if(mathJaxDeferred.state() != 'resolved'){
                    mathJaxDeferred.resolve();
                }
            }
        });

        $.when(presenter.pageLoaded, mathJaxProcessEnded).then(function() {
            const upgradedModel = presenter.upgradeModel(model);
            presenter.configuration = presenter.validateModel(upgradedModel, isPreview);
            if (presenter.configuration.isError) {
                DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
                return;
            }
            presenter.configuration.$inputs = $(presenter.configuration.workWithViews).find('input').not('.ic_text_audio_button');


            if (!isPreview) {
                if (presenter.configuration.customLayout.length &gt; 0) {
                    try {
                        eval('presenter.configuration.customLayout = ' + presenter.configuration.customLayout);
                    } catch (e) {
                        presenter.ERROR_CODES['evaluationError'] = e.message;
                        DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, 'evaluationError');
                    }
                }

                if (presenter.configuration.customDisplay.length &gt; 0) {
                    try {
                        eval('presenter.configuration.customDisplay = ' + presenter.configuration.customDisplay);
                    } catch(e) {
                        presenter.ERROR_CODES['evaluationError'] = e.message;
                        DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, 'evaluationError');
                    }
                }
                presenter.configuration.customLayout.id = new Date().getTime();

                var defaultDisplay = {
                    a      : '\u2714:Accept (Shift-Enter)', // check mark - same action as accept
                    accept : 'Accept:Accept (Shift-Enter)',
                    alt    : 'AltGr:Alternate Graphemes',
                    b      : '\u2190:Backspace',    // Left arrow (same as &amp;larr;)
                    bksp   : 'Bksp:Backspace',
                    c      : '\u2716:Cancel (Esc)', // big X, close - same action as cancel
                    cancel : 'Cancel:Cancel (Esc)',
                    clear  : 'C:Clear',             // clear num pad
                    combo  : '\u00f6:Toggle Combo Keys',
                    dec    : '.:Decimal',           // decimal point for num pad (optional), change '.' to ',' for European format
                    e      : '\u21b5:Enter',        // down, then left arrow - enter symbol
                    enter  : 'Enter:Enter',
                    left   : '\u2190',              // left arrow (move caret)
                    lock   : '\u21ea Lock:Caps Lock', // caps lock
                    next   : 'Next',
                    prev   : 'Prev',
                    right  : '\u2192',              // right arrow (move caret)
                    s      : '\u21e7:Shift',        // thick hollow up arrow
                    shift  : 'CapsLock:CapsLock',
                    sign   : '\u00b1:Change Sign',  // +/- sign for num pad
                    space  : '&amp;nbsp;:Space',
                    t      : '\u21e5:Tab',          // right arrow to bar (used since this virtual keyboard works with one directional tabs)
                    tab    : '\u21e5 Tab:Tab'       // \u21b9 is the true tab symbol (left &amp; right arrows)
                };

                var customDisplay = presenter.configuration.customDisplay;
                presenter.display = $.extend(defaultDisplay, customDisplay);

                if (MobileUtils.isMobileUserAgent(navigator.userAgent) &amp;&amp; presenter.configuration.lockInput) {
                    presenter.configuration.$inputs.each(
                        function (index, element) {
                            var $el = $(element);
                            $el.addClass('ui-keyboard-lockedinput');
                            $el.attr('readonly', true);
                            $el.attr('inputmode', "none");
                        }
                    );
                }

                presenter.removeEventListeners();

                presenter.connectHandlers();
            }
            for (var i = 0; i &lt; presenter.functionsQueue.length; i++) {
                presenter.functionsQueue[i]();
            }
            presenter.isLoaded = true;
        });
    }

    /**
     * Adds handlers to all input elements with which eKeyboard works
     */

    presenter.connectHandlers = function AddoneKeyboard_connectHandlers() {
        presenter.configuration.$inputs.each(
            function (index, element) {
                if (DevicesUtils.isInternetExplorer()) {
                    element.addEventListener('mousedown', presenter.focusOnMouseDown);
                }

                element.addEventListener('focus', presenter.openEKeyboardOnFocus);
                element.addEventListener('forceClick', presenter.openEKeyboardOnForceClick);
                element.addEventListener('keyup', presenter.onESCHideKeyboard);

                if (presenter.configuration.maxCharacters !== false) {
                    element.addEventListener('change', presenter.moveToNextGap);
                    element.addEventListener('paste', presenter.moveToNextGap);
                    element.addEventListener('keyup', presenter.moveToNextGap);
                }

                //This is after setState because validateModel is in promise.
                if (!keyboardIsVisible) {
                    element.addEventListener('focusout', focusoutCallBack);
                }
            });
    };

    presenter.focusOnMouseDown = function AddoneKeyboard_focusOnMouseDown () {
        $(this).focus();
    };

    presenter.openEKeyboardOnFocus = function AddoneKeyboard_openEKeyboardOnFocus () {
        lastClickedElement = this;
        if (!keyboardIsVisible) {
            if ($(this).data('keyboard') !== undefined) {
                $(this).data('keyboard').destroy();
            }
            openButtonElement.style.display = 'block';
            actualizeOpenButtonPosition($(lastClickedElement));
        } else {
            presenter.createEKeyboard(this, presenter.display);
            $(this).trigger('showKeyboard');
        }
    };

    presenter.openEKeyboardOnForceClick = function AddoneKeyboard_openEKeyboardOnForceClick() {
        if (presenter.configuration.openOnFocus) {
            $(this).data('keyboard').reveal();
            if ($(".ic_popup_page").length == 0) {
                $(this).data('keyboard').startup();
            }
        } else {
            $(this).focus();
        }
    };

    presenter.onESCHideKeyboard = function AddoneKeyboard_onESCHideKeyboard(e) {
        var isEKeyboardOpen = $(this).data('keyboard') &amp;&amp; $(this).data('keyboard').isOpen;
        if (e.keyCode === 27 &amp;&amp; isEKeyboardOpen) {
            onEscClick();
        }
    };

    presenter.moveToNextGap = function AddoneKeyboard_moveToNextGap() {
        if ($(this).val().length &gt;= presenter.configuration.maxCharacters) {
            var self = this;
            $(this).val($(this).val().substring(0, presenter.configuration.maxCharacters));

            if ($(this).data('keyboard') !== undefined) {
                //Fix bug with events
                setTimeout(function () {
                    $(self).data('keyboard').switchInput(true, true);
                }, 0);
            } else {
                lastClickedElement = this;
                movedInput = true;
                getNextFocusableElement(this, true).focus();
            }
        }
    };

    presenter.clickedOutsideCallback = function AddoneKeyboard_clickedOutsideCallback(event) {
        // shouldn't hide keyboard when current input was clicked
        if (event.target === lastClickedElement) return;

        var wrapper = $(presenter.keyboardWrapper);

        // if click outside of wrapper or it's descendant, hide keyboard
        if (!wrapper.is(event.target) &amp;&amp; wrapper.has(event.target).length === 0) {
            presenter.hideKeyboard();
        }
    };

    presenter.hideKeyboard = function () {
        document.removeEventListener('mousedown', presenter.clickedOutsideCallback);

        $(closeButtonElement).hide();
        $(lastClickedElement).removeAttr("readonly");
        $(lastClickedElement).removeAttr("inputmode");
        var keyboard = $(lastClickedElement).data('keyboard');
        if (keyboard !== undefined) {
            keyboard.accept();
        }
    };

    presenter.createEKeyboard = function (element, display) {
        if ($(element).data('keyboard') !== undefined) {
            return;
        }

        $(element).keyboard({
            // *** choose layout ***
            layout: presenter.configuration.layoutType,
            customLayout: presenter.configuration.customLayout,
            position: {
                of : null, // optional - null (attach to input/textarea) or a jQuery object (attach elsewhere)
                my : presenter.configuration.positionMy.value,
                at : presenter.configuration.positionAt.value,
                at2 : presenter.configuration.positionAt.value,
                offset : presenter.configuration.offset.value,
                collision: 'fit'
            },

                    // preview added above keyboard if true, original input/textarea used if false
                    usePreview: false,

                    // if true, the keyboard will always be visible
                    alwaysOpen: false,

                    // give the preview initial focus when the keyboard becomes visible
                    initialFocus: presenter.configuration.lockInput,

                    // if true, keyboard will remain open even if the input loses focus.
                    stayOpen: true,

                    // *** change keyboard language &amp; look ***
                    display: display,

                    // Message added to the key title while hovering, if the mousewheel plugin exists
                    wheelMessage: 'Use mousewheel to see other keys',

                    css: {
                        input          : '', //'ui-widget-content ui-corner-all', // input &amp; preview
                        container      : 'ui-widget-content ui-widget ui-corner-all ui-helper-clearfix', // keyboard container
                        buttonDefault  : 'ui-state-default ui-corner-all', // default state
                        buttonHover    : 'ui-state-hover',  // hovered button
                        buttonAction   : 'ui-state-active', // Action keys (e.g. Accept, Cancel, Tab, etc); replaces "actionClass"
                        buttonDisabled : 'ui-state-disabled' // used when disabling the decimal button {dec}
                    },

                    // *** Useability ***
                    // Auto-accept content when clicking outside the keyboard (popup will close)
                    autoAccept: true,

                    // Prevents direct input in the preview window when true
                    lockInput: presenter.configuration.lockInput,

                    // Prevent keys not in the displayed keyboard from being typed in
                    restrictInput: false,

                    // Check input against validate function, if valid the accept button is clickable;
                    // if invalid, the accept button is disabled.
                    acceptValid: true,

                    // Use tab to navigate between input fields
                    tabNavigation: true,

                    // press enter (shift-enter in textarea) to go to the next input field
                    enterNavigation : true,
                    // mod key options: 'ctrlKey', 'shiftKey', 'altKey', 'metaKey' (MAC only)
                    enterMod : 'altKey', // alt-enter to go to previous; shift-alt-enter to accept &amp; go to previous

                    // if true, the next button will stop on the last keyboard input/textarea; prev button stops at first
                    // if false, the next button will wrap to target the first input/textarea; prev will go to the last
                    stopAtEnd : false,

                    // Set this to append the keyboard immediately after the input/textarea it is attached to.
                    // This option works best when the input container doesn't have a set width and when the
                    // "tabNavigation" option is true
                    appendLocally: false,

            appendTo: presenter.keyboardWrapper,

                    // If false, the shift key will remain active until the next key is (mouse) clicked on;
                    // if true it will stay active until pressed again
                    stickyShift: true,

                    // Prevent pasting content into the area
                    preventPaste: false,

                    // Set the max number of characters allowed in the input, setting it to false disables this option
                    //maxLength: presenter.configuration.maxCharacters,

                    // Mouse repeat delay - when clicking/touching a virtual keyboard key, after this delay the key
                    // will start repeating
                    repeatDelay: 500,

                    // Mouse repeat rate - after the repeatDelay, this is the rate (characters per second) at which the
                    // key is repeated. Added to simulate holding down a real keyboard key and having it repeat. I haven't
                    // calculated the upper limit of this rate, but it is limited to how fast the javascript can process
                    // the keys. And for me, in Firefox, it's around 20.
                    repeatRate: 20,

                    // resets the keyboard to the default keyset when visible
                    resetDefault: false,

                    // Event (namespaced) on the input to reveal the keyboard. To disable it, just set it to ''.
                    openOn: presenter.configuration.openOnFocus ? 'showKeyboard' : '',

                    // When the character is added to the input
                    keyBinding: 'touchend mousedown',

                    // combos (emulate dead keys : http://en.wikipedia.org/wiki/Keyboard_layout#US-International)
                    // if user inputs `a the script converts it to à, ^o becomes ô, etc.
                    useCombos: false,

                    // if true, keyboard will not close if you press escape.
                    ignoreEsc : true,

                    autoAcceptOnEsc : false,
                    // *** Methods ***
                    // Callbacks - add code inside any of these callback functions as desired
                    initialized: function (e, keyboard, el) {
                    },
                    beforeVisible: function (e, keyboard, el) {
                        if (!keyboard['$keyboard'].parent().hasClass('html')) {
                            var dialogBox = keyboard['$keyboard'].parent().find('.gwt-DialogBox');
                            dialogBox.append(keyboard['$keyboard']);
                        }

                        var parent = keyboard['$keyboard'].parent(),
                            popup = parent.find('.ic_popup');

                        if (popup.length &gt; 0) {
                            popup.append(keyboard['$keyboard']);
                        }
                    },
                    visible: function (e, keyboard, el) {
                        var isVisibleInViewPort = getIsVisibleInViewPort(keyboard['$keyboard']);
                        if (!isVisibleInViewPort) {
                            return;
                        }

                        if (!isVisibleInViewPort.vertical || !isVisibleInViewPort.horizontal) {
                            shiftKeyboard(keyboard, isVisibleInViewPort);
                        }

                        keyboard['$keyboard'].draggable({
                            drag: function () {
                                $(closeButtonElement).position({
                                    my:        "left top",
                                    at:        "right top",
                                    of:         keyboard['$keyboard'],
                                    collision: 'fit'
                                });
                            },
                            stop: function () {
                                $.ui.ddmanager.current = null;
                            }
                        });

                        var $keyboard = keyboard['$keyboard'];
                        var position = $keyboard.position();

                        var widthMargin = ($keyboard.outerWidth(true) -  $keyboard.innerWidth()) / 2;
                        var width = $keyboard.outerWidth() + widthMargin;

                        var heightMargin = ($keyboard.outerHeight(true) -  $keyboard.innerHeight()) / 2;

                        $(closeButtonElement).css({
                            top: position.top + heightMargin + 'px',
                            left: position.left + width + 'px'
                        });

                        showCloseButton();

                        document.addEventListener('mousedown', presenter.clickedOutsideCallback);
                    },
                    change: function (e, keyboard, el) {
                        var api = $(lastClickedElement).data('keyboard');

                        //Fixing the issue where if a key contains word 'meta' it will be treated as a meta key
                        if (api.last.key &amp;&amp; api.last.key.indexOf('meta') != -1
                            &amp;&amp; presenter.configuration.customLayout[api.last.key] == null) {
                            keyboard.insertText(api.last.key);
                        }

                        var event = new Event('change');
                        el.dispatchEvent(event);

                    },
                    beforeClose: function(e, keyboard, el, accepted) {
                        document.removeEventListener('mousedown', presenter.clickedOutsideCallback);
                        $(closeButtonElement).hide();
                    },
                    accepted: function(e, keyboard, el) {},
                    canceled: function(e, keyboard, el) {},
                    hidden: function(e, keyboard, el) {},

                    switchInput : function(keyboard, goToNext, isAccepted){
                        var base = keyboard, kb, stopped = false,
                            all = $('input, textarea').filter(':enabled');
                        all = all.filter(function (element) {
                            return presenter.addonIsWorkingWithElement(all.eq(element))
                        });
                        var index = all.index(base.$el) + (goToNext ? 1 : -1);
                        if (index &gt; all.length - 1) {
                            stopped = keyboard.stopAtEnd;
                        }
                        if (index &lt; 0) {
                            stopped = keyboard.stopAtEnd;
                            index = all.length - 1; // stop or go to last
                        }
                        if (!stopped) {
                            if (!base.close(isAccepted)) {
                                return;
                            }
                            if (presenter.addonIsWorkingWithElement(all.eq(index))) {
                                    presenter.createEKeyboard(all.eq(index), display);
                            }
                            if (keyboardIsVisible) {
                                all.eq(index).trigger('forceClick');
                            }
                            if($(".ic_popup_page").length === 0){
                                all.eq(index).focus();
                            }
                        }

                        return false;
                	},
                    // this callback is called just before the "beforeClose" to check the value
                    // if the value is valid, return true and the  will continue as it should
                    // (close if not always open, etc)
                    // if the value is not value, return false and the clear the keyboard value
                    // ( like this "keyboard.$preview.val('');" ), if desired
                    // The validate function is called after each input, the "isClosing" value will be false;
                    // when the accept button is clicked, "isClosing" is true
                    validate: function (keyboard, value, isClosing) {
                        return true;
                    }
                });
                $(lastClickedElement).trigger('forceClick');
            };

    function getNextFocusableElement (element, next) {
        var all = $('input, textarea').filter(':enabled');
        var indx = all.index(element) + (next ? 1 : -1);

        if (indx &gt; all.length - 1) {
            indx = 0; // go to first input
        }
        if (indx &lt; 0) {
            indx = all.length - 1; // stop or go to last
        }
        return all.eq(indx);

    }

    presenter.addonIsWorkingWithElement = function (element) {
        return ($(presenter.configuration.workWithViews).find(element).length != 0);
    };

    function asyncFunctionDecorator(func) {
        if (presenter.isLoaded) {
            func();
        } else {
            presenter.functionsQueue.push(func);
        }
    }

    function hideOpenButton() {
        openButtonElement.style.display = 'none';
    }

    function focusoutCallBack(ev) {
        if (!keyboardIsVisible &amp;&amp; !movedInput) {
            hideOpenButton();
        }
        movedInput = false;
        ev.preventDefault();
    }

    function showButtonDecorator(func) {
        if (presenter.configuration.showCloseButton || presenter.isShowCloseButton) {
            func();
        }
    }

    function showCloseButton() {
        showButtonDecorator(function () {
            $(closeButtonElement).show();
        });
    }

    function closeButtonCallBack() {
        presenter.disable();

        $(lastClickedElement).focus();
        $(lastClickedElement).click();

        document.removeEventListener('mousedown', presenter.clickedOutsideCallback);
        $(closeButtonElement).hide();
    }

    function showOpenButtonCallback() {
        hideOpenButton();
        presenter.configuration.$inputs.attr("inputmode", "none");
        presenter.enable();

        escClicked = false;

        document.activeElement.blur();

        $(lastClickedElement).click();
        $(lastClickedElement).focus();
        $(lastClickedElement).trigger('showKeyboard');
    }

    function actualizeOpenButtonPosition(element) {
        $(openButtonElement).position({
            of: element,
            my: presenter.configuration.positionMy.value,
            at: presenter.configuration.positionAt.value,
            at2: presenter.configuration.positionAt.value,
            offset: presenter.configuration.offset.value,
            collision: 'flip'
        });
    }

    function shiftKeyboard(keyboard, isVisibleInViewPort) {
        if (!isVisibleInViewPort.horizontal) {
            var currentLeft = parseInt(keyboard['$keyboard'].css('left'), 10);
            keyboard['$keyboard'].css('left', currentLeft + parseInt(isVisibleInViewPort.horizontalSign + '10', 10));
        }
        if (!isVisibleInViewPort.vertical) {
            var currentTop = parseInt(keyboard['$keyboard'].css('top'), 10);
            keyboard['$keyboard'].css('top', currentTop + parseInt(isVisibleInViewPort.verticalSign + '10', 10));
        }
    }

    function getIsVisibleInViewPort(element) {
        var $window = $(window);

        if (this.length &lt; 1)
            return;

        if ($(element).length == 0) {
            return;
        }

        var $element = $(element),
            vpWidth = $window.width(),
            vpHeight = $window.height(),
            viewTop = $window.scrollTop(),
            viewBottom = viewTop + vpHeight,
            viewLeft = $window.scrollLeft(),
            viewRight = viewLeft + vpWidth,
            offset = $element.offset(),
            _top = offset.top,
            _bottom = _top + $element.height(),
            _left = offset.left,
            _right = _left + $element.width();

        return {
            vertical: ((_bottom &lt;= viewBottom) &amp;&amp; (_top &gt;= viewTop)),
            horizontal: ((_right &lt;= viewRight) &amp;&amp; (_left &gt;= viewLeft)),
            verticalSign: _bottom &lt;= viewBottom ? '' : '-',
            horizontalSign: _right &lt;= viewRight ? '' : '-'
        };
    }

    function onEscClick() {
        closeButtonCallBack();

        openButtonElement.style.display = 'block';
        actualizeOpenButtonPosition($(lastClickedElement));
    }

    presenter.run = function(view, model){
        runLogic(view, model, false);
    };

    presenter.setShowErrorsMode = function(){
    };

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isError) {
            return;
        }

        var commands = {
            'open' : presenter.openCommand,
            'disable' : presenter.disable,
            'enable' : presenter.enable,
            'showCloseButton' : presenter.showCloseButton
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.showCloseButton = function () {
        presenter.isShowCloseButton = true;
    };

    presenter.disable = function (){
        asyncFunctionDecorator(presenter.disableFunc.bind(this));
    };

    presenter.disableFunc = function () {
        presenter.sendEvent("disable");
        if (presenter.configuration.openOnFocus) {
            keyboardIsVisible = false;
        }

        presenter.configuration.$inputs.each(function (index, element) {
            try {
                $(element).data('keyboard').destroy();
            } catch(err){}
        });

        document.removeEventListener('mousedown', presenter.clickedOutsideCallback);

        presenter.configuration.$inputs.on('focusout', focusoutCallBack);
        presenter.configuration.$inputs.removeClass('ui-keyboard-input ui-keyboard-input-current');
        presenter.configuration.$inputs.removeAttr("readonly");
        presenter.configuration.$inputs.removeAttr("inputmode");
    };

    presenter.enable = function () {
        asyncFunctionDecorator(presenter.enableFunc.bind(this));
    };

    presenter.enableFunc = function () {
        presenter.sendEvent("enable");
        keyboardIsVisible = true;
        $(presenter.configuration.workWithViews).find('input').off('focusout', focusoutCallBack);
    };

    presenter.open = function (moduleId, index) {
        asyncFunctionDecorator(presenter.openFunc.bind(this, moduleId, index));
    };

    presenter.openFunc = function(moduleId, index) {
        var module = presenter.playerController.getModule(moduleId);
        try {
            var input = $(module.getView()).find('input:enabled').get(parseInt(index, 10) - 1);
            presenter.createEKeyboard(input, presenter.display);
            $(input).data('keyboard').reveal();
        } catch (e) {
        }

    };

    presenter.openCommand = function(moduleId, index) {
        if ($.isArray(moduleId)) {
            presenter.open(moduleId[0], moduleId[1]);
        } else {
            presenter.open(moduleId, index);
        }
    };

    presenter.sendEvent = function (status) {
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': '',
            'value': status
        });
    };

    presenter.destroy = function destroy_addon_eKeyboard_function (event) {
        if (presenter.isPreview || !presenter.configuration || presenter.configuration.ID !== event.target) {
            return;
        }

        presenter.configuration.$inputs.off('focusout', focusoutCallBack);

        presenter.configuration.$inputs.each(function (index, element){
            try {
                $(element).data('keyboard').destroy();
                $(element).off('focusout change paste keyup forceClick focus mousedown');
            } catch(err){}
        });

        document.removeEventListener('mousedown', presenter.clickedOutsideCallback);
        $(presenter.keyboardWrapper).remove();
        $(openButtonElement).remove();
    };

    presenter.setWorkMode = function(){
    };

    presenter.reset = function(){
    };

    presenter.getErrorCount = function(){
        return 0;
    };

    presenter.getMaxScore = function(){
        return 0;
    };

    presenter.getScore = function(){
        return 0;
    };

    presenter.getState = function () {
        return JSON.stringify({
            "isClosed": keyboardIsVisible,
            "isShowCloseButton": presenter.isShowCloseButton
        });
    };

    presenter.setState = function (state) {
        var parsedState = JSON.parse(state);
        keyboardIsVisible = parsedState.isClosed;

        if(parsedState.isShowCloseButton != undefined) {
            presenter.isShowCloseButton = parsedState.isShowCloseButton;
        }
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="feedback" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Responses" nameLabel="feedback_property_responses" type="list">
			<property name="Unique response ID" nameLabel="feedback_property_unique_response_id" type="string"/>
			<property name="Status" nameLabel="feedback_property_status" type="string"/>
			<property isLocalized="true" name="Text" nameLabel="feedback_property_text" type="html"/>
		</property>
		<property isLocalized="true" name="Default response" nameLabel="feedback_property_default_response" type="html"/>
		<property name="Preview response ID" nameLabel="feedback_property_preview_response_id" type="string"/>
		<property name="Reset response on page change" nameLabel="feedback_property_reset_response_on_page_change" type="boolean"/>
		<property name="Fade transitions" nameLabel="feedback_property_fade_transitions" type="boolean"/>
		<property name="Center horizontally" nameLabel="feedback_property_center_horizontally" type="boolean"/>
		<property name="Center vertically" nameLabel="feedback_property_center_vertically" type="boolean"/>
		<property name="Is not an activity" nameLabel="feedback_property_is_not_an_activity" type="boolean"/>
		<property name="Mute" nameLabel="feedback_property_mute" type="boolean"/>
		<property name="langAttribute" nameLabel="feedback_property_lang_attribute" type="string"/>
        <property name="speechTexts" nameLabel="feedback_property_speech_texts" type="staticlist">
            <property name="Positive" nameLabel="feedback_property_positive" type="staticrow">
                <property name="Positive" nameLabel="feedback_property_positive" type="string"/>
            </property>
            <property name="Negative" nameLabel="feedback_property_negative" type="staticrow">
                <property name="Negative" nameLabel="feedback_property_negative" type="string"/>
            </property>
			<property name="Empty" nameLabel="feedback_property_no_feedback" type="staticrow">
                <property name="Empty" nameLabel="feedback_property_no_feedback" type="string"/>
            </property>
		</property>
	</model>
<css>.feedback_container .response {
    padding: 1em;
    position: absolute;
    width: 100%;
}

.feedback_container .response.visible a {
    z-index: 1;
}

.feedback_container .true_response {
    border: 2px solid #00aa00;
    border-radius: 4px;
}

.feedback_container .false_response {
    border: 2px solid #ff0000;
    border-radius: 4px;
}
</css><view/><preview/><presenter>function Addonfeedback_create() {
    var presenter = function () {
    };

    presenter.responses = {};
    presenter.defaultResponse = "";
    presenter.feedbackContainer = null;
    presenter.currentStateDefault = false;
    presenter.currentStateId = null;
    presenter.speechTexts = {};

    var playerController = null;
    var textParser = null;

    presenter.STATUSES = {
        TRUE: "T",
        FALSE: "F",
        NEUTRAL: "N",
        CUSTOM: "C"
    };

    presenter.ERROR_MESSAGES = {
        RESPONSE_ID_NOT_UNIQUE: "Response ID \"%id%\" is not unique",
        RESPONSE_STATUS_INVALID: "Response status \"%status%\" for response %n% (ID \"%id%\") is invalid, it has to be one of \"T\" (true), \"N\" (neutral), \"F\" (false) or \"C\" (custom)",
        PREVIEW_RESPONSE_ID_INVALID: "Cannot preview response: there's no response with ID \"%id%\""
    };

    presenter.setPlayerController = function (controller) {
        playerController = controller;
        textParser = new TextParserProxy(playerController.getTextParser());
    };

    function getTextVoiceObject (text, lang) {return {text: text, lang: lang};}

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    var feedbackSpeakInterval = null;
    function setSpeakInterval (data) {
        if(feedbackSpeakInterval) {
            clearInterval(feedbackSpeakInterval);
            feedbackSpeakInterval = null;
        }
        feedbackSpeakInterval = setInterval(function () {

            var speechSynthSpeaking = false;
            var responsiveVoiceSpeaking = false;
            if ('speechSynthesis' in window) {
                speechSynthSpeaking = window.speechSynthesis.speaking;
            }
            if (window.responsiveVoice) {
                responsiveVoiceSpeaking = window.responsiveVoice.isPlaying();
            }
            if (!speechSynthSpeaking &amp;&amp; !responsiveVoiceSpeaking) {
                clearInterval(feedbackSpeakInterval);
                feedbackSpeakInterval = null;
                var tts = presenter.getTextToSpeechOrNull(playerController);
                if (tts &amp;&amp; playerController.isWCAGOn()) {
                    tts.speak(data);
                }
            }
        }, 200);
    }

    var feedbackSpeakTimeout = null;
    function speak (data) {
        if(feedbackSpeakTimeout) {
            clearTimeout(feedbackSpeakTimeout);
            feedbackSpeakTimeout = null;
        }
        feedbackSpeakTimeout = setTimeout(function(){setSpeakInterval(data);},300);
    }

    presenter.stopFeedbackTTS = function() {
        clearTimeout(feedbackSpeakTimeout);
        clearInterval(feedbackSpeakInterval);
    };

    function getTextVoiceArrayFromText(text) {
        const el = document.createElement('div');
        el.innerHTML = window.xssUtils.sanitize(text);
        const $el = $(el);

        return window.TTSUtils.getTextVoiceArrayFromElement($el,presenter.configuration.langTag);
    }

    presenter.readCurrentMessage = function(readEmptyOnDefault) {
        if (presenter.getTextToSpeechOrNull(playerController) &amp;&amp; playerController.isWCAGOn()) {
            if (presenter.currentStateDefault) {
                if(readEmptyOnDefault) {
                    presenter.readDefaultMessage();
                } else {
                    presenter.stopFeedbackTTS();
                }
            } else if (presenter.currentStateId) {
                presenter.readMessageById(presenter.currentStateId);
            }
        }
    };

    presenter.readDefaultMessage = function() {
        var TextVoiceArray = [];
        TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.empty));
        speak(TextVoiceArray);
    };

    presenter.readMessageById = function(id) {
        if(id &amp;&amp; presenter.responses[id]) {
            var TextVoiceArray = [];
            var response = presenter.responses[id];
            if ( 0 === response.status.toLowerCase().localeCompare("t")) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.positive));
            }
            if ( 0 === response.status.toLowerCase().localeCompare("f")) {
                TextVoiceArray.push(getTextVoiceObject(presenter.speechTexts.negative));
            }
            TextVoiceArray = TextVoiceArray.concat(getTextVoiceArrayFromText(response.text));
            speak(TextVoiceArray);
        }
    };

    presenter.showErrorMessage = function (message, substitutions) {
        var errorContainer;
        if (typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for (var key in substitutions) {
                if (!substitutions.hasOwnProperty(key)) continue;

                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.$view.html(errorContainer);
    };

    presenter.setDefaultResponse = function () {
        if (!presenter.preview) {
            presenter.feedbackContainer.find('.response').removeClass('visible');
            presenter.feedbackContainer.find('.default_response').addClass('visible');

            if (presenter.configuration.isTabindexEnabled) {
                presenter.setTabindex(presenter.feedbackContainer.find('.response'), false);
                presenter.setTabindex(presenter.feedbackContainer.find('.default_response'), true);
            }
        }
        if (presenter.preview || !presenter.configuration.fadeTransitions) {
            presenter.feedbackContainer.find('.response').css('opacity', 0);
            presenter.feedbackContainer.find('.default_response').css('opacity', 1);
        } else if (presenter.configuration.fadeTransitions) {
            presenter.feedbackContainer.find('.response:not(.default_response)').animate({opacity: 0.0}, {queue: false});
            presenter.feedbackContainer.find('.default_response').animate({opacity: 1.0}, {queue: false});
        }
        presenter.currentStateDefault = true;
        presenter.currentStateId = null;

        if (!presenter.configuration.isMute) {
            presenter.readCurrentMessage(false);
        }
    };

    presenter.setResponse = function (id) {
        if (!presenter.preview) {
            presenter.feedbackContainer.find('.response').removeClass('visible');
            presenter.feedbackContainer.find('.response_' + id).addClass('visible');

            if (presenter.configuration.isTabindexEnabled) {
                presenter.setTabindex(presenter.feedbackContainer.find('.response'), false);
                presenter.setTabindex(presenter.feedbackContainer.find('.response_' + id), true);
            }
        }
        if (presenter.preview || !presenter.configuration.fadeTransitions) {
            presenter.feedbackContainer.find('.response').css('opacity', 0);
            presenter.feedbackContainer.find('.response_' + id).appendTo(presenter.feedbackContainer).css('opacity', 1);
        } else if (presenter.configuration.fadeTransitions) {
            presenter.feedbackContainer.find('.response').animate({opacity: 0.0}, {queue: false});
            presenter.feedbackContainer.find('.response_' + id).appendTo(presenter.feedbackContainer).animate({opacity: 1.0}, {queue: false});
        }

        presenter.currentStateDefault = false;
        presenter.currentStateId = id;

        if (!presenter.configuration.isMute) {
            presenter.readCurrentMessage(false);
        }
    };


    function createResponseTable(content) {
        var $feedbackTable = $(document.createElement('table'));
        var $feedbackTableRow = $(document.createElement('tr'));
        var $feedbackTableCell = $(document.createElement('td'));

        $feedbackTableRow.append($feedbackTableCell);
        $feedbackTable.append($feedbackTableRow);
        $feedbackTableCell.html(content);

        return $feedbackTable;
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeFrom_01(model);
        return presenter.upgradeAddMute(upgradedModel);
    };

    presenter.upgradeFrom_01 = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = "";
        }
        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]["Positive"]) {
            upgradedModel["speechTexts"]["Positive"] = {Positive: ""};
        }
        if (!upgradedModel["speechTexts"]["Negative"]) {
            upgradedModel["speechTexts"]["Negative"] = {Negative: ""};
        }
        if (!upgradedModel["speechTexts"]["Empty"]) {
            upgradedModel["speechTexts"]["Empty"] = {Empty: ""};
        }
        return upgradedModel;
    };

    presenter.upgradeAddMute = function(model) {
            var upgradedModel = {};
            $.extend(true, upgradedModel, model);

            if (!upgradedModel["Mute"]) {
                upgradedModel["Mute"] = "False";
            }
            return upgradedModel;
        };

    presenter.initialize = function (view, model, preview) {
        var text;
        var text_inner;

        model = presenter.upgradeModel(model);

        presenter.$view = $(view);
        presenter.preview = preview;
        presenter.model = model;

        presenter.feedbackContainer = $('&lt;div class="feedback_container"&gt;&lt;/div&gt;');
        presenter.$view.append(presenter.feedbackContainer);

        presenter.configuration = presenter.validateModel(model);

        for (var i = 0; i &lt; model['Responses'].length; i++) {
            if (typeof(presenter.responses[model['Responses'][i]['Unique response ID']]) != "undefined") {
                presenter.showErrorMessage(presenter.ERROR_MESSAGES.RESPONSE_ID_NOT_UNIQUE, { id: model['Responses'][i]['Unique response ID']});
                return;
            }

            if (model['Responses'][i]['Status'] != presenter.STATUSES.TRUE &amp;&amp;
                model['Responses'][i]['Status'] != presenter.STATUSES.NEUTRAL &amp;&amp;
                model['Responses'][i]['Status'] != presenter.STATUSES.FALSE &amp;&amp;
                model['Responses'][i]['Status'] != presenter.STATUSES.CUSTOM) {
                presenter.showErrorMessage(presenter.ERROR_MESSAGES.RESPONSE_STATUS_INVALID, { n: i + 1, id: model['Responses'][i]['Unique response ID'], status: model['Responses'][i]['Status']});
                return;
            }

            var responseText = model['Responses'][i]['Text'];
            if (textParser !== null &amp;&amp; !preview) {
                responseText = textParser.parse(responseText);
            }
            presenter.responses[model['Responses'][i]['Unique response ID']] = { status: model['Responses'][i]['Status'].toUpperCase(), text: responseText };

            text = $('&lt;div class="response"&gt;&lt;/div&gt;');
            text_inner = $('&lt;div class="response_inner"&gt;&lt;/div&gt;');
            text.append(text_inner);
            text.addClass('response_' + model['Responses'][i]['Unique response ID']);

            var $responseTable = createResponseTable(responseText);
            text_inner.html($responseTable);

            switch (model['Responses'][i]['Status']) {
                case presenter.STATUSES.TRUE:
                    text.addClass("true_response");
                    break;

                case presenter.STATUSES.NEUTRAL:
                    text.addClass("neutral_response");
                    break;

                case presenter.STATUSES.FALSE:
                    text.addClass("false_response");
                    break;

                case presenter.STATUSES.CUSTOM:
                    text.addClass("custom_response");
                    break;
            }

            presenter.feedbackContainer.append(text);
            presenter.centerInner(text_inner);
        }

        presenter.$view.find('.response').css('opacity', 0.0);

        presenter.defaultResponse = model['Default response'];
        if (textParser !== null &amp;&amp; !preview) {
            presenter.defaultResponse = textParser.parse(presenter.defaultResponse);
        }

        text = $('&lt;div class="response default_response neutral_response"&gt;&lt;/div&gt;');
        text_inner = $('&lt;div class="response_inner"&gt;&lt;/div&gt;');
        text.append(text_inner);

        var defaultResponseTable = createResponseTable(presenter.defaultResponse);
        text_inner.html(defaultResponseTable);
        presenter.feedbackContainer.append(text);
        presenter.centerInner(text_inner);
        if (textParser !== null &amp;&amp; !preview) {
            textParser.connectLinks(presenter.feedbackContainer);
            presenter.connectHTMLLinks(presenter.feedbackContainer);
        }

        if (!preview) {
            presenter.setDefaultResponse();
            return;
        }

        if (typeof(model['Preview response ID']) == "undefined" ||
            (typeof(model['Preview response ID']) == "string" &amp;&amp; model['Preview response ID'] == "")) {
            presenter.setDefaultResponse();
        } else {
            if (typeof(presenter.responses[model['Preview response ID']]) == "undefined") {
                presenter.showErrorMessage(presenter.ERROR_MESSAGES.PREVIEW_RESPONSE_ID_INVALID, { id: model['Preview response ID'] });
            } else {
                presenter.setResponse(model['Preview response ID']);
            }
        }
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            positive:  'Correct answer',
            negative: 'Incorrect answer',
            empty: 'No feedback message'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            positive:    getSpeechTextProperty(speechTexts['Positive']['Positive'], presenter.speechTexts.positive),
            negative: getSpeechTextProperty(speechTexts['Negative']['Negative'], presenter.speechTexts.negative),
            empty: getSpeechTextProperty(speechTexts['Empty']['Empty'], presenter.speechTexts.empty)
        };
    }

    presenter.validateModel = function (model) {
        var validatedIsVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        setSpeechTexts(model["speechTexts"]);

        return {
            resetResponse: ModelValidationUtils.validateBoolean(model['Reset response on page change']),
            fadeTransitions: ModelValidationUtils.validateBoolean(model['Fade transitions']),
            centerHorizontally: ModelValidationUtils.validateBoolean(model['Center horizontally']),
            centerVertically: ModelValidationUtils.validateBoolean(model['Center vertically']),
            isActivity: !ModelValidationUtils.validateBoolean(model['Is not an activity']),
            isVisible: validatedIsVisible,
            isVisibleByDefault: validatedIsVisible,
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model['Is Tabindex Enabled']),
            isMute: ModelValidationUtils.validateBoolean(model['Mute']),
            langTag: model['langAttribute']
        };
    };

    presenter.centerInner = function (text_inner) {
        if (presenter.configuration.centerVertically) {
            var parentHeight = $(text_inner).parent().height();
            var parentHWidth = $(text_inner).parent().width();
            $(text_inner).css({
                height: parentHeight + 'px',
                width: parentHWidth + 'px',
                display: 'table-cell',
                'verticalAlign': 'middle'
            });
        }

        if (presenter.configuration.centerHorizontally) {
            $(text_inner).find('table').css({
                marginLeft: 'auto',
                marginRight: 'auto'
            });
        }
    };

    presenter.run = function (view, model) {
        presenter.initialize(view, model, false);
    };

    presenter.createPreview = function (view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.reset = function () {
        presenter.setDefaultResponse();
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
    };

    presenter.setShowErrorsMode = function () {
        if (presenter.configuration.isActivity) {
            presenter.setDefaultResponse();
        }
    };

    presenter.change = function (responseID) {
        if (presenter.responses[responseID] !== undefined) {
            presenter.setResponse(responseID);
        }
    };

    presenter.changeCommand = function (params) {
        if (params.length &gt;= 1) {
            presenter.change(params[0])
        }
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'change': presenter.changeCommand,
            'setDefaultResponse': presenter.setDefaultResponse,
            'next': presenter.next,
            'previous': presenter.previous,
            'getResponseIndex': presenter.getResponseIndex
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.next = function () {
        var currentID = presenter.currentStateId, index, newID;
        if (presenter.currentStateDefault) {
            newID = presenter.getResponseID(0);
        } else {
            index = presenter.getResponseIndex(currentID);
            newID = presenter.getResponseID(index + 1);
        }

        if (newID == undefined) return;

        presenter.setResponse(newID);
    };

    presenter.previous = function () {
        var currentID = presenter.currentStateId, index, newID;

        if (presenter.currentStateDefault) return;

        index = presenter.getResponseIndex(currentID);
        newID = presenter.getResponseID(index - 1);

        if (newID == undefined) return;

        presenter.setResponse(newID);
    };

    presenter.connectHTMLLinks = function ($element) {
        $element.find('a').each(function(){
            $(this).click(function(){
                var url = $(this).attr('href');
                var target = $(this).attr('target');
                if (target != null &amp;&amp; target.length &gt; 0) {
                    window.open(url, target);
                } else {
                    window.open(url);
                }
            });
        });
    };


    presenter.getState = function () {
        return JSON.stringify({
            'currentStateDefault': presenter.currentStateDefault,
            'currentStateId': presenter.currentStateId,
            'isVisible': presenter.configuration.isVisible
        });
    };

    presenter.setState = function (stateString) {
        if (presenter.configuration.resetResponse) {
            presenter.setDefaultResponse();
            return;
        }
        var state = JSON.parse(stateString);

        if (state['currentStateDefault']) {
            presenter.setDefaultResponse();
        } else {
            presenter.setResponse(state['currentStateId']);
        }

        presenter.configuration.isVisible = state['isVisible'];
        presenter.setVisibility(state['isVisible']);
    };

    presenter.getResponseIndex = function (responseID) {
        var responses = presenter.model.Responses, i;

        for (i = 0; i &lt; responses.length; i++) {
            if (responses[i]['Unique response ID'] === responseID) return i;
        }

        return -1;
    };

    presenter.getResponseID = function (index) {
        var response = presenter.model.Responses[index];

        return response !== undefined ? response['Unique response ID'] : undefined;
    };

    presenter.setTabindex = function ($element, isTabindexEnabled){
        var value = isTabindexEnabled ? '0' : '-1';
        $element.attr('tabindex', value);
    };

    presenter.keyboardController = function(keyCode, isShift, event) { //every time keyboard is pressed
        if (keyCode == window.KeyboardControllerKeys.SPACE ||
            keyCode == window.KeyboardControllerKeys.ARROW_UP ||
            keyCode == window.KeyboardControllerKeys.ARROW_DOWN ||
            keyCode == window.KeyboardControllerKeys.ESC)
        {
            event.preventDefault();
        }

        if (keyCode == window.KeyboardControllerKeys.ENTER) {
            presenter.readCurrentMessage(true);
        }
    };

    presenter.isEnterable = function() {
        return false;
    };

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="gamememo" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Pairs" nameLabel="gamememo_property_pairs" type="list">
			<property isLocalized="true" name="A (text)" nameLabel="gamememo_property_pair_a_text" type="string"/>
			<property name="A (image)" nameLabel="gamememo_property_pair_a_image" type="image"/>
			<property name="A (alt text)" nameLabel="gamememo_property_alt_text_style_a" type="string"/>
			<property isLocalized="true" name="B (text)" nameLabel="gamememo_property_pair_b_text" type="string"/>
			<property name="B (image)" nameLabel="gamememo_property_pair_b_image" type="image"/>
			<property name="B (alt text)" nameLabel="gamememo_property_alt_text_style_a" type="string"/>
		</property>
		<property name="Columns" nameLabel="gamememo_property_columns" type="string"/>
		<property name="Rows" nameLabel="gamememo_property_rows" type="string"/>
		<property name="Use two styles for cards" nameLabel="gamememo_property_use_two_styles_for_cards" type="boolean"/>
		<property name="Image for style A" nameLabel="gamememo_property_image_for_style_a" type="image"/>
		<property name="Image for style B" nameLabel="gamememo_property_image_for_style_b" type="image"/>
		<property name="Style A cover alt text" nameLabel="gamememo_property_alt_text_style_a" type="string"/>
		<property name="Style B cover alt text" nameLabel="gamememo_property_alt_text_style_b" type="string"/>
		<property name="Keep cards aspect ratio" nameLabel="gamememo_property_keep_cards_aspect_ratio" type="boolean"/>
		<property name="Show cards for preview" nameLabel="gamememo_property_show_cards_for_preview" type="boolean"/>
        <property name="Is Not Activity" nameLabel="gamememo_property_is_not_activity" type="boolean"/>
        <property name="Image Mode" nameLabel="gamememo_property_image_mode" type="{Original, Stretch, KeepAspect}"/>
        <property name="Keep wrong marking" nameLabel="gamememo_property_keep_wrong_marking" type="boolean"/>
        <property name="Time to solve" nameLabel="gamememo_property_time_to_solve" type="string"/>
        <property name="Session ended message" nameLabel="gamememo_property_session_ended_message" type="html"/>
		<property name="Click to turn over incorrect pair" nameLabel="gamememo_property_click_to_turn_over_incorrect_pair" type="boolean"/>
		<property name="Show all answers in gradual show answers mode" nameLabel="gamememo_property_show_all_answers_in_gradual_show_answers_mode" type="boolean"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Connection_property_lang_attribute" type="string"/>
        <property displayName="Speech texts" name="speechTexts" nameLabel="Connection_property_speech_texts" type="staticlist">

            <property name="Revealed" nameLabel="gamememo_revealed" type="staticrow">
                <property name="Revealed" nameLabel="gamememo_revealed" type="string"/>
            </property>

			<property name="Paired" nameLabel="gamememo_paired" type="staticrow">
                <property name="Paired" nameLabel="gamememo_paired" type="string"/>
            </property>

			<property name="Value" nameLabel="gamememo_value_of" type="staticrow">
                <property name="Value" nameLabel="gamememo_value_of" type="string"/>
            </property>

			<property name="WrongColor" nameLabel="gamememo_wrong_color" type="staticrow">
                <property name="WrongColor" nameLabel="gamememo_wrong_color" type="string"/>
            </property>

			<property name="Match" nameLabel="gamememo_match" type="staticrow">
                <property name="Match" nameLabel="gamememo_match" type="string"/>
            </property>

			<property name="NotMatch" nameLabel="gamememo_not_match" type="staticrow">
                <property name="NotMatch" nameLabel="gamememo_not_match" type="string"/>
            </property>

			<property name="CurrentlySelected" nameLabel="gamememo_currently_selected" type="staticrow">
                <property name="CurrentlySelected" nameLabel="gamememo_currently_selected" type="string"/>
            </property>

			<property name="TurnOver" nameLabel="gamememo_turn_over" type="staticrow">
                <property name="TurnOver" nameLabel="gamememo_turn_over" type="string"/>
            </property>

			<property name="OutOf" nameLabel="gamememo_out_of" type="staticrow">
                <property name="OutOf" nameLabel="gamememo_out_of" type="string"/>
            </property>

			<property name="Found" nameLabel="gamememo_found" type="staticrow">
                <property name="Found" nameLabel="gamememo_found" type="string"/>
            </property>

			<property name="RevealedCards" nameLabel="gamememo_revealed_cards" type="staticrow">
                <property name="RevealedCards" nameLabel="gamememo_revealed_cards" type="string"/>
            </property>

		</property>
	</model>
<css>.gamememo_container table {
    border-spacing: 0;
    width: 100%;
    height: 100%;
}

.gamememo_container table td {
    padding: 0;
    margin: 0;
    position: relative;
}

.gamememo_container table td img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

}

.gamememo_container p {
    text-align: center;
}

.gamememo_container div.cell {
    float: left;
    position: relative;
    overflow: hidden;
}


.gamememo_container div.placeholder {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    overflow: hidden;
}

.gamememo_container div.front {
    cursor: pointer;
}

.gamememo_container div.front_A {
    background: #ff0000;
}

.gamememo_container div.front_A:hover {
    background: #aa0000;
}

.gamememo_container div.front_B {
    background: #0000ff;
}

.gamememo_container div.front_B:hover {
    background: #0000bb;
}

.gamememo_container div.mismatch_mark {
    color: #cc0000;
    font-weight: bold;
    font-size: 64pt;
    text-align: center;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    vertical-align: middle;
    margin-top: -0.15ex;
    cursor: default;
    line-height: normal;
}

.gamememo_container .memo-lock-screen {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: #cc0000;
    opacity: 0.5;
}

.gamememo_container .memo-lock-screen-info {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    margin-top: 10%;
    text-align: center;
    color: #ffffff;
}

.gamememo_container div.tick_mark {
    color: #11cc11;
    font-weight: bold;
    font-size: 64pt;
    text-align: center;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    vertical-align: middle;
    margin-top: -0.15ex;
    cursor: default;
    line-height: normal;
}

.gamememo_container .keyboard_navigation_active_element {
    box-shadow: inset 0px 0px 0px 2px #00ff44 !important;
  	outline: none;
}

.gamememo_container span.gamememo_alt_text {
    clip: rect(1px, 1px, 1px, 1px);
    height: 1px;
    width: 1px;
    overflow: hidden;
    position: absolute !important;
}
</css><view>&lt;div&gt;&lt;/div&gt;
</view><preview>&lt;div&gt;&lt;/div&gt;
</preview><presenter>function Addongamememo_create(){
    var presenter = function(){};

    var playerController;
    var textParser;
    var eventBus;
    var keyboardController = null;
    var isWCAGOn = false;
    var screenLocked = false;

    presenter.isShowErrorsMode = false;

    function MemoKeyboardController(elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }
    MemoKeyboardController.prototype = Object.create(KeyboardController.prototype);
    MemoKeyboardController.prototype.constructor = MemoKeyboardController;

    MemoKeyboardController.prototype.getTarget = function (element) {
        return element.find('.placeholder');
    };

    MemoKeyboardController.prototype.selectAction = function () {
        if (!this.keyboardNavigationCurrentElement.hasClass('was-clicked')) {
            this.keyboardNavigationCurrentElement.find('.front.placeholder').click();
        }
    };

    MemoKeyboardController.prototype.nextElement = function (event) {
        if (screenLocked) return;
        if (event.keyCode == 9) {
            return this.handleTab(event);
        }
        if ((this.keyboardNavigationCurrentElementIndex + 1) % presenter.columnCount !== 0) {
            KeyboardController.prototype.nextElement.call(this, event);
            presenter.readCurrentCell(false);
        }
    };

    MemoKeyboardController.prototype.previousElement = function (event) {
        if (screenLocked) return;
        if (event.keyCode == 9) {
            return this.handleTab(event);
        }
         if (this.keyboardNavigationCurrentElementIndex % presenter.columnCount !== 0) {
             KeyboardController.prototype.previousElement.call(this, event);
             presenter.readCurrentCell(false);
         }

    };

    MemoKeyboardController.prototype.nextRow = function (event) {
        event.preventDefault();
        if (screenLocked) return;
        if (this.keyboardNavigationCurrentElementIndex + presenter.columnCount &lt; this.keyboardNavigationElementsLen) {
             KeyboardController.prototype.nextRow.call(this, event);
             presenter.readCurrentCell(false);
         }
    };

    MemoKeyboardController.prototype.previousRow = function (event) {
        event.preventDefault();
        if (screenLocked) return;
        if (this.keyboardNavigationCurrentElementIndex - presenter.columnCount &gt;= 0) {
             KeyboardController.prototype.previousRow.call(this, event);
             presenter.readCurrentCell(false);
         }
    };

    MemoKeyboardController.prototype.enter = function (event) {
        var wasActive = this.keyboardNavigationActive;
        KeyboardController.prototype.enter.call(this, event);
        if(event) {
            if (!event.shiftKey) {
                if (!screenLocked) {
                    if (!wasActive &amp;&amp; presenter.isShowErrorsMode) {
                        var TextVoices = presenter.getCellTextVoices(this.keyboardNavigationCurrentElementIndex, {prefix: true, color: true});
                        TextVoices = TextVoices.concat(presenter.getCompletionTextVoices());
                        presenter.speak(TextVoices);
                    } else {
                        presenter.readCurrentCell(true);
                    }
                } else {
                    var $container = $('&lt;div&gt;&lt;/div&gt;');
                    $container.html(presenter.sessionEndedMessage);
                    var TextVoices = window.TTSUtils.getTextVoiceArrayFromElement($container, presenter.configuration.langTag);
                    presenter.speak(TextVoices);
                }
            }
        }
    };

    MemoKeyboardController.prototype.handleTab = function (event) {
        if (event) {
            if (!event.shiftKey) {
                var nextIndex = this.getNextActiveElementIndex();
                if (nextIndex != null &amp;&amp; nextIndex != this.keyboardNavigationCurrentElementIndex) {
                    this.switchElement(nextIndex - this.keyboardNavigationCurrentElementIndex);
                    presenter.readCurrentCell(false);
                }
            } else {
                var prevIndex = this.getPreviousActiveElementIndex();
                if (prevIndex != null &amp;&amp; prevIndex != this.keyboardNavigationCurrentElementIndex) {
                    this.switchElement(prevIndex - this.keyboardNavigationCurrentElementIndex);
                    presenter.readCurrentCell(false);
                }
            }
        }
    };

    MemoKeyboardController.prototype.getPreviousActiveElementIndex = function () {
      var currentIndex = this.keyboardNavigationCurrentElementIndex - 1;
      while ( currentIndex &gt;= 0) {
          if (presenter.isCardActive(currentIndex)) {
              return currentIndex;
          } else {
              currentIndex = currentIndex - 1;
          }
      }
      return null;
    };

    MemoKeyboardController.prototype.getNextActiveElementIndex = function () {
      var currentIndex = this.keyboardNavigationCurrentElementIndex + 1;
      while ( currentIndex &lt; this.keyboardNavigationElementsLen) {
          if (presenter.isCardActive(currentIndex)) {
              return currentIndex;
          } else {
              currentIndex = currentIndex + 1;
          }
      }
      return null;
    };

    MemoKeyboardController.prototype.resetPosition = function () {
        this.switchElement(0 - this.keyboardNavigationCurrentElementIndex);
    };

    presenter.isCardActive = function(index) {
        if (index &lt; 0 || index &gt;= keyboardController.keyboardNavigationElementsLen) return false;

        if (presenter.isShowAnswersActive) return true;

        // test if card is already revealed
        var $card = $(keyboardController.keyboardNavigationElements[index]);
        if ($card.is('.was-clicked') || $card.find('.was-clicked').length &gt; 0) return false;

        // test if card has the same style as the one currently clicked
        if (presenter.useTwoStyles &amp;&amp; presenter.state == presenter.STATES.CLICKED_FIRST) {
            var serializedCard = presenter.serializedCards[index];
            var clickedStyle = 0;
            if (presenter.cardClickedStyle == 'B') clickedStyle = 1;
            if (serializedCard.cardStyle == clickedStyle) return false;
        }

        return true;
    };

    presenter.readCurrentCell = function (extraData) {
        var index = keyboardController.keyboardNavigationCurrentElementIndex;

        // SHOW ANSWERS MODE
        if(presenter.isShowAnswersActive) {
            var serializedFirstCard = presenter.serializedCards[index];
            var secondIndex = -1;
            for (var i = 0; i &lt; presenter.serializedCards.length; i++) {
                if(i != index &amp;&amp; presenter.serializedCards[i].cardId == serializedFirstCard.cardId) {
                    secondIndex = i;
                    break;
                }
            }
            if (secondIndex &lt; 0) {
                presenter.speak(presenter.getCellTextVoices(index, {prefix: true, color: true, value: true}));
                return;
            }
            var TextVoices = presenter.getCellTextVoices(index, {prefix: false, color: false, value: extraData});
            TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.match));
            TextVoices = TextVoices.concat(presenter.getCellTextVoices(secondIndex, {prefix: false, color: false, value: extraData}));
            presenter.speak(TextVoices);
            return;
        }

        // SHOW ERRORS MODE
        if (presenter.isShowErrorsMode) {
            var readValue = extraData || !presenter.serializedCards[index].revealed;

            var TextVoices = presenter.getCellTextVoices(index, {prefix: true, color: true, value: readValue});
            var serializedFirstCard = presenter.serializedCards[index];
            if (serializedFirstCard.revealed) {
                var secondIndex = -1;
                for (var i = 0; i &lt; presenter.serializedCards.length; i++) {
                    if (i != index &amp;&amp; presenter.serializedCards[i].cardId == serializedFirstCard.cardId) {
                        secondIndex = i;
                        break;
                    }
                }
                if (secondIndex &lt; 0) {
                    presenter.speak(presenter.getCellTextVoices(index, {prefix: true, color: true, value: true}));
                    return;
                }
                TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.match));
                TextVoices = TextVoices.concat(presenter.getCellTextVoices(secondIndex, {prefix: false, color: extraData, value: extraData}));
            }
            if (extraData) {
                if (presenter.state == presenter.STATES.CLICKED_SECOND) {
                    if (!presenter.serializedCards[index].revealed) {
                        var $card = $(keyboardController.keyboardNavigationElements[index]);
                        if (($card.is('.was-clicked') || $card.find('.was-clicked').length &gt; 0)) {
                            var revealedCards = presenter.getRevealedCards();
                            var secondIndex = -1;
                            if (revealedCards[0] === index) {
                                secondIndex = revealedCards[1];
                            } else {
                                secondIndex = revealedCards[0];
                            }
                            TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.notMatch));
                            TextVoices = TextVoices.concat(presenter.getCellTextVoices(secondIndex, {prefix: false, color: true, value: true}));
                        }
                    }
                }
                TextVoices = TextVoices.concat(presenter.getCompletionTextVoices());
            }

            presenter.speak(TextVoices);
            return;
        }

        // WORK MODE

        // read the value when providing extra data or when the cell is revealed, but not paired
        var readValue = extraData || !presenter.serializedCards[index].revealed;

        TextVoices = presenter.getCellTextVoices(index, {prefix: true, color: true, value: readValue});
        if (presenter.serializedCards[index].revealed) {
            var secondIndex = -1;
                for (var i = 0; i &lt; presenter.serializedCards.length; i++) {
                    if (i != index &amp;&amp; presenter.serializedCards[i].cardId == presenter.serializedCards[index].cardId) {
                        secondIndex = i;
                        break;
                    }
                }
                if (secondIndex &gt; -1) {
                    TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.match));
                    TextVoices = TextVoices.concat(presenter.getCellTextVoices(secondIndex, {prefix: false, color: extraData, value: extraData}));
                }
        }
        if (extraData) {
            if (presenter.state == presenter.STATES.CLICKED_FIRST) {
                var selectedIndex = presenter.getCardIndex(presenter.cardClickedFirst);
                if (index != selectedIndex) {
                    TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.currentlySelected));
                    var currentlySelectedVoiceIndex = TextVoices.length - 1;
                    TextVoices = TextVoices.concat(presenter.getCellTextVoices(presenter.getCardIndex(presenter.cardClickedFirst), {prefix: false, color: true, value: true}));
                    if (TextVoices.length &gt; currentlySelectedVoiceIndex+1) {
                        TextVoices[currentlySelectedVoiceIndex].text += " " + TextVoices[currentlySelectedVoiceIndex+ 1 ].text;
                        TextVoices.splice(currentlySelectedVoiceIndex + 1, 1);
                    }
                }
            } else if (presenter.state == presenter.STATES.CLICKED_SECOND) {
                if (!presenter.serializedCards[index].revealed) {
                    var $card = $(keyboardController.keyboardNavigationElements[index]);
                    if (($card.is('.was-clicked') || $card.find('.was-clicked').length &gt; 0)) {
                        var revealedCards = presenter.getRevealedCards();
                        var secondIndex = -1;
                        if (revealedCards[0] === index) {
                            secondIndex = revealedCards[1];
                        } else {
                            secondIndex = revealedCards[0];
                        }
                        TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.notMatch));
                        TextVoices = TextVoices.concat(presenter.getCellTextVoices(secondIndex, {prefix: false, color: true, value: true}));
                    }
                }
            }
            TextVoices = TextVoices.concat(presenter.getCompletionTextVoices());
        }
        presenter.speak(TextVoices);
    };

    /*
    * Returns an array of numbers, each indicating the index of a card that is revealed, but not paired
    * */
    presenter.getRevealedCards = function() {
        var foundCards = [];
        for (var i = 0; i &lt; keyboardController.keyboardNavigationElementsLen; i++) {
            var $card = $(keyboardController.keyboardNavigationElements[i]);
            if (!presenter.serializedCards[i].revealed &amp;&amp; ($card.is('.was-clicked') || $card.find('.was-clicked').length &gt; 0)) {
                foundCards.push(i);
            }
        }
        return foundCards;
    };

    presenter.getCardIndex = function($card) {
        var query = '';
        if ($card.hasClass('cell')) {
            query = '.cell';
        } else if ($card.hasClass('card')) {
            query = '.card';
        } else {
            return null;
        }
        var cardIndex = -1;
        presenter.viewContainer.find(query).each(function(index){
            if ($(this).is($card)) cardIndex = index;
        });
        if (cardIndex &gt;= 0) return cardIndex;
        return null;
    };

    /*
    * get TextVoice array for a specified cell
    *
    * Parameters
    *   index - cell index in keyboardNavigationElements array ( it should be the same in cards and serializedCards )
    *   args - an object specifying any additional information that should be passed to the TextVoices array. It can
    *       have following fields:
    *       prefix - add 'revealed' or 'paired' at the beginning where applicable
    *       color - if using two colors, read the color alt text before cell's coordinates
    *       value - read the value of the cell
    *
    * Returns
    *   an Array of TextVoiceObjects
    * */
    presenter.getCellTextVoices = function(index, args) {
        if (args == null) args = {};
        if(args.prefix == null) {
            args.prefix = false;
        }
        if(args.color == null) {
            args.color = false;
        }
        if(args.value == null) {
            args.value = false;
        }
        if (index == null || index &lt; 0 || index &gt;= keyboardController.keyboardNavigationElementsLen) return [];

        var row = Math.floor(index / presenter.columnCount) + 1;
        var alphabet = "ABCDEFGHIJKLMNOPRSTQWXYZ";
        var columnIndex = index % presenter.columnCount;
        var columnLetter = alphabet[columnIndex % alphabet.length];
        var serializedCard = presenter.serializedCards[index];
        var styleAlt = '';
        if(args.color  &amp;&amp; presenter.useTwoStyles) {
            if (serializedCard.cardStyle == 1) {
                styleAlt = presenter.configuration.altTextStyleB;
            } else {
                styleAlt = presenter.configuration.altTextStyleA;
            }
        }
        var title = styleAlt + ' ' + columnLetter + row;

        var $card = $(keyboardController.keyboardNavigationElements[index]);
        var revealed = ($card.is('.was-clicked') || $card.find('.was-clicked').length &gt; 0);

        var TextVoices = [];

        if (args.prefix &amp;&amp; revealed) {
            if (serializedCard.revealed) {
                TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.paired));
            } else {
                TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.revealed));
            }
        }

        TextVoices.push(window.TTSUtils.getTextVoiceObject(title));

        if (args.value &amp;&amp; (revealed || $card.hasClass('cell-show-answers'))) {
            TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.value));

            var content = '';
            if (serializedCard.type == "text") {
                content = serializedCard.content;
            } else {
                content = presenter.cards[index].attr('alt');
            }
            TextVoices.push(window.TTSUtils.getTextVoiceObject(content, presenter.configuration.langTag));
        }

        return TextVoices;
    };

    presenter.getCompletionTextVoices = function() {
       var total = presenter.serializedCards.length / 2;
       var found = 0;
       for ( var i = 0; i &lt; presenter.serializedCards.length; i++) {
           if (presenter.serializedCards[i].revealed) found += 1;
       }
       found = found / 2;

       var revealed = presenter.getRevealedCards().length;

       var TextVoices = [];
       TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.found));
       TextVoices.push(window.TTSUtils.getTextVoiceObject(String(found)));
       TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.outOf));
       TextVoices.push(window.TTSUtils.getTextVoiceObject(String(total)));
       if (!presenter.isShowAnswersActive) {
           TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.revealedCards));
           TextVoices.push(window.TTSUtils.getTextVoiceObject(String(revealed)));
       }
       return TextVoices;
    };

    presenter.setPlayerController = function (controller) {
        playerController = controller;
        textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setEventBus = function (wrappedEventBus) {
        eventBus = wrappedEventBus;
    };

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.numberToCardType = function(n) {
        if(n == 0) {
            return 'A';
        } else if(n == 1) {
            return 'B';
        } else {
            throw new Error('presenter.numberToCardType accepts only values 0 and 1');
        }
    };

    // Fisher-Yates algorithm
    // based on http://sedition.com/perl/javascript-fy.html
    presenter.shuffleArray = function(a) {
        var i = a.length;
        if ( i == 0 ) return [];

        while ( --i ) {
            var j = Math.floor( Math.random() * ( i + 1 ) );
            var tempi = a[i];
            a[i] = a[j];
            a[j] = tempi;
        }

        return a;
    };

    // Fisher-Yates algorithm
    // based on http://sedition.com/perl/javascript-fy.html
    presenter.shuffleTwoArrays = function(a1, a2) {
        var i = a1.length;
        if ( i == 0 ) return [[],[]];

        while ( --i ) {
            var j = Math.floor( Math.random() * ( i + 1 ) );

            var temp1i = a1[i];
            a1[i] = a1[j];
            a1[j] = temp1i;

            var temp2i = a2[i];
            a2[i] = a2[j];
            a2[j] = temp2i;
        }

        return [a1, a2];
    };

    presenter.STATES = {
        READY: 0,
        CLICKED_FIRST: 1,
        CLICKED_SECOND: 2
    };

    presenter.state = presenter.STATES.READY;

    presenter.cardClickedFirst = null;
    presenter.cardClickedSecond = null;
    presenter.cardClickedFirstId = null;
    presenter.cardClickedSecondId = null;
    presenter.cardClickedStyle = null;
    presenter.areClickedCardsHiddenInGSAMode = false;

    presenter.errorCount = 0;
    presenter.score = 0;
    presenter.maxScore = null;
    presenter.timeToSolve = 0;
    presenter.timer = null;
    presenter.sessionStarted = null;
    presenter.spendTime = 0;

    presenter.preview = false;
    presenter.previewCards = false;

    presenter.cards = [];
    presenter.serializedCards = [];
    presenter.rowCount = null;
    presenter.columnCount = null;
    presenter.useTwoStyles = false;
    presenter.keepAspectRatio = false;
    presenter.styleAImage = null;
    presenter.styleBImage = null;

    presenter.model = null;

    presenter.requestedRowHeight = null;
    presenter.requestedColumnWidth = null;

    presenter.ERROR_MESSAGES = {
        PAIRS_NOT_SPECIFIED: "Pairs are not specified",
        ROWS_NOT_SPECIFIED: "Amount of rows is not specified",
        COLUMNS_NOT_SPECIFIED: "Amount of columns is not specified",
        INVALID_GEOMETRY: "Invalid amount of columns and/or rows - their multiplication must be even",
        AMOUNT_OF_PAIRS_OTHER_THAN_GEOMETRY: "Invalid amount of pairs: for %columns% columns and %rows% rows there should be defined %pairs% pairs",
        PAIR_MEMBER_SPECIFIED_TWICE: "Pair %pair% is invalid: its member \"%member%\" is specified both as a text and an image",
        PAIR_MEMBERS_NOT_SPECIFIED: "Pair %pair% is invalid: its members are not specified"
    };

    presenter.showErrorMessage = function(message, substitutions) {
        var errorContainer;
        if(typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for(var key in substitutions) {
                if (!substitutions.hasOwnProperty(key)) continue;
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.viewContainer.html(errorContainer);
    };

    function getTimeToSolve(model) {
        var tts = model['Time to solve'];
        // parse time to solve and return it in seconds
        tts = parseInt(tts, 10);
        return tts
    }

    presenter.upgradeModel = function(model) {
        var upgradedModel = presenter.upgradeModelAddTTS(model);
        upgradedModel = upgradeModelAddShowAllAnswersInGSAModeProperty(upgradedModel);
        return upgradedModel;
    };

    presenter.upgradeModelAddTTS = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel['langAttribute']) {
            upgradedModel['langAttribute'] = '';
        }

        if (!upgradedModel['speechTexts']) {
            upgradedModel['speechTexts'] = {
                Revealed: {Revealed: "Revealed"},
                Paired: {Paired: "Paired"},
                Value: {Value: "with a value of"},
                WrongColor: {WrongColor: "Incorrect card color"},
                Match: {Match: "Matches"},
                NotMatch: {NotMatch: "Doesn't match"},
                CurrentlySelected: {CurrentlySelected: "Currently selected"},
                TurnOver: {TurnOver: "Incorrect pair was turned over"},
                OutOf: {OutOf: "Out of"},
                Found: {Found: "Found"},
                RevealedCards: {RevealedCards: "Number of revealed cards"}
            }
        }

        return upgradedModel;
    };

    function upgradeModelAddShowAllAnswersInGSAModeProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['Show all answers in gradual show answers mode']){
            upgradedModel['Show all answers in gradual show answers mode'] = false;
        }

        return upgradedModel;
    }

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    presenter.setSpeechTexts = function(speechTexts) {
        presenter.speechTexts = {
            revealed:  'Revealed',
            paired: 'Paired',
            value: 'with a value of',
            wrongColor: 'Incorrect card color',
            match: 'Matches',
            notMatch: 'Doesn\'t match',
            currentlySelected: 'Currently selected',
            turnOver: 'Incorrect pair was turned over',
            outOf: 'out of',
            found: 'found',
            revealedCards: 'Number of revealed cards'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            revealed:    getSpeechTextProperty(speechTexts['Revealed']['Revealed'], presenter.speechTexts.revealed),
            paired: getSpeechTextProperty(speechTexts['Paired']['Paired'], presenter.speechTexts.paired),
            value:  getSpeechTextProperty(speechTexts['Value']['Value'], presenter.speechTexts.value),
            wrongColor:     getSpeechTextProperty(speechTexts['WrongColor']['WrongColor'], presenter.speechTexts.wrongColor),
            match:   getSpeechTextProperty(speechTexts['Match']['Match'], presenter.speechTexts.match),
            notMatch:      getSpeechTextProperty(speechTexts['NotMatch']['NotMatch'], presenter.speechTexts.notMatch),
            currentlySelected:        getSpeechTextProperty(speechTexts['CurrentlySelected']['CurrentlySelected'], presenter.speechTexts.currentlySelected),
            turnOver:        getSpeechTextProperty(speechTexts['TurnOver']['TurnOver'], presenter.speechTexts.turnOver),
            outOf:        getSpeechTextProperty(speechTexts['OutOf']['OutOf'], presenter.speechTexts.outOf),
            found:        getSpeechTextProperty(speechTexts['Found']['Found'], presenter.speechTexts.found),
            revealedCards:        getSpeechTextProperty(speechTexts['RevealedCards']['RevealedCards'], presenter.speechTexts.revealedCards)
        };
    };

    presenter.validateModel = function(model) {
        presenter.setSpeechTexts(model['speechTexts']);

        if(model['Pairs'].length == 0) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.PAIRS_NOT_SPECIFIED
            };
        }

        if(parseInt(model['Columns']) &lt;= 0 || isNaN(parseInt(model['Columns'])) ) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.COLUMNS_NOT_SPECIFIED
            };
        }

        if(parseInt(model['Rows']) &lt;= 0 || isNaN(parseInt(model['Rows']))) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.ROWS_NOT_SPECIFIED
            };
        }

        if((parseInt(model['Rows']) * parseInt(model['Columns'])) % 2 == 1) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.INVALID_GEOMETRY
            };
        }

        if(parseInt(model['Rows']) * parseInt(model['Columns']) != model['Pairs'].length * 2) {
            return {
                isError: true,
                errorMessage: presenter.ERROR_MESSAGES.AMOUNT_OF_PAIRS_OTHER_THAN_GEOMETRY,
                errorMessageSubstitutions: {
                    rows: parseInt(model['Rows']),
                    columns: parseInt(model['Columns']),
                    pairs: parseInt(model['Rows']) * parseInt(model['Columns']) / 2
                }
            };
        }


        for(var i = 0; i &lt; model['Pairs'].length; i++) {
            if(model['Pairs'][i]['A (text)'] == "" &amp;&amp; model['Pairs'][i]['A (image)'] == "" &amp;&amp;
                model['Pairs'][i]['B (text)'] == "" &amp;&amp; model['Pairs'][i]['B (image)'] == "") {
                return {
                    isError: true,
                    errorMessage: presenter.ERROR_MESSAGES.PAIR_MEMBERS_NOT_SPECIFIED,
                    errorMessageSubstitutions: {
                        pair: i + 1
                    }
                };
            }
            for(var n = 0; n &lt;= 1; n++) {
                if(model['Pairs'][i][presenter.numberToCardType(n) + ' (text)'] != "" &amp;&amp; model['Pairs'][i][presenter.numberToCardType(n) + ' (image)'] != "") {
                    return {
                        isError: true,
                        errorMessage: presenter.ERROR_MESSAGES.PAIR_MEMBER_SPECIFIED_TWICE,
                        errorMessageSubstitutions: {
                            pair: i + 1,
                            member: presenter.numberToCardType(n)
                        }
                    };
                }
            }
        }

        presenter.maxScore = model['Pairs'].length;
        presenter.rowCount = parseInt(model['Rows']);
        presenter.columnCount = parseInt(model['Columns']);
        presenter.useTwoStyles = model['Use two styles for cards'] == 'True';
        presenter.keepAspectRatio = model['Keep cards aspect ratio'] == 'True';
        presenter.previewCards = model['Show cards for preview'] == 'True';
        presenter.styleAImage = model['Image for style A'] != '' ? model['Image for style A'] : null;
        presenter.styleBImage = model['Image for style B'] != '' ? model['Image for style B'] : null;
        presenter.isActivity = !(ModelValidationUtils.validateBoolean(model['Is Not Activity']));
        presenter.imageMode = model['Image Mode'];
        presenter.keppWrongMarking = ModelValidationUtils.validateBoolean(model['Keep wrong marking']);
        presenter.timeToSolve = getTimeToSolve(model);
        presenter.sessionEndedMessage = model['Session ended message'];

        var viewWidth = parseInt(presenter.viewContainer.css('width'));
        var viewHeight = parseInt(presenter.viewContainer.css('height'));

        presenter.requestedColumnWidth = Math.round(viewWidth / presenter.columnCount);
        presenter.requestedRowHeight = Math.round(viewHeight / presenter.rowCount);


        return {
            isError: false,
            ID: model.ID,
            pairs: model['Pairs'],
            isVisible: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model['Is Visible']),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]),
            clickToTurnOverIncorrectPair: ModelValidationUtils.validateBoolean(model["Click to turn over incorrect pair"]),
            showAllAnswersInGradualShowAnswersMode: ModelValidationUtils.validateBoolean(model["Show all answers in gradual show answers mode"]),
            altTextStyleA: model['Style A cover alt text'],
            altTextStyleB: model['Style B cover alt text'],
            langTag: model['langAttribute']
        };
    };

    presenter.slideUpAnimation = function ($element, successFunction) {
        var distance = $element.outerHeight();
        $element.animate({bottom: (distance + 'px')}, 200, function () {
            $(this).css({
                visibility: 'hidden',
                bottom: 0
            });

            successFunction();
        });
    };

    presenter.slideDownAnimation = function ($element, successFunction) {
        var distance = $element.outerHeight();

        if(presenter.imageMode == 'KeepAspect') {
            $element.css('visibility', 'visible');
        } else {
             $element.css({'bottom': (distance + 'px'), visibility: 'visible'})
                .animate({bottom: 0}, 200);
        }
    };

    presenter.showCard = function(cell) {
        var successFunction = function () {
            presenter.slideDownAnimation(cell.children(".back"));
        };

        presenter.slideUpAnimation(cell.children(".front"), successFunction);

        if(!presenter.isShowAnswersActive){
            cell.addClass('was-clicked');
        }
    };

    presenter.hideCard = function(cell) {
        var successFunction = function () {
            presenter.slideDownAnimation(cell.children(".front"));
        };

        presenter.slideUpAnimation(cell.children(".back"), successFunction);
        cell.removeClass('exposed-in-GSA-mode');

        if(!presenter.isShowAnswersActive){
            cell.removeClass('was-clicked');
        }
    };

    presenter.showClickedCardsInGSAMode = function () {
        if (presenter.areClickedCardsHiddenInGSAMode) {
            if (presenter.cardClickedFirst) {
                presenter.showCard(presenter.cardClickedFirst);
                presenter.cardClickedFirst.addClass('was-clicked');
            }
            if (presenter.cardClickedSecond) {
                presenter.showCard(presenter.cardClickedSecond);
                presenter.cardClickedSecond.addClass('was-clicked');
            }
            presenter.areClickedCardsHiddenInGSAMode = false;
        }
    }

    presenter.hideClickedCardsInGSAMode = function () {
        if (!presenter.areClickedCardsHiddenInGSAMode) {
            if (presenter.cardClickedFirst) {
                presenter.hideCard(presenter.cardClickedFirst);
                presenter.cardClickedFirst.removeClass('was-clicked');
            }
            if (presenter.cardClickedSecond) {
                presenter.hideCard(presenter.cardClickedSecond);
                presenter.cardClickedSecond.removeClass('was-clicked');
            }
            presenter.areClickedCardsHiddenInGSAMode = true;
        }
    }

    function getMarkDiv(markType, height) {
        var factory = {
            "tick": function () {
                return $('&lt;div class="tick_mark"&gt;&amp;check;&lt;/div&gt;');
            },

            "mismatch": function () {
                return $('&lt;div class="mismatch_mark"&gt;&amp;times;&lt;/div&gt;');
            }
        };

        var mark = factory[markType]();

        mark.css({
            opacity: 0.8,
            fontSize: Math.round(parseInt(height) * 0.95) + 'px'
        });

        return mark;
    }

    function fadeOutMark (mark, time) {
        mark.fadeOut(time, function () {
            mark.remove();
        });
    }

    function setMarkHeight (mark) {
        mark.css('top', Math.round(parseInt(mark.css('height')) * -0.08) + 'px');
    }

    presenter.markCardMismatch = function(cell, heightProbeCell) {
        var mark = getMarkDiv("mismatch", heightProbeCell.parent().css('height'));

        cell.parent().append(mark);

        setMarkHeight(mark);
        if (!presenter.keppWrongMarking) {
            fadeOutMark(mark, 1300)
        }
    };

    presenter.markCardTick = function(cell, heightProbeCell) {
        var mark = getMarkDiv("tick", heightProbeCell.parent().css('height'));
        cell.parent().append(mark);

        setMarkHeight(mark);
        fadeOutMark(mark, 1300)
    };


    presenter.prepareGrid = function() {
        var cards = [];
        var serializedCards = [];
        var card;
        var serializedCard;

        var pairs = presenter.configuration.pairs;

        for(var n = 0; n &lt;= 1; n++) {
            for(var j = 0; j &lt; pairs.length; j++) {

                if(pairs[j][presenter.numberToCardType(n) + ' (text)'] != "") {
                    card = $('&lt;p&gt;&lt;/p&gt;').text(pairs[j][presenter.numberToCardType(n) + ' (text)']);
                    serializedCard = { revealed: false, type: "text", content: pairs[j][presenter.numberToCardType(n) + ' (text)'] }

                } else {
                    card = $('&lt;img/&gt;').attr({ src: pairs[j][presenter.numberToCardType(n) + ' (image)']});

                    var altText = pairs[j][presenter.numberToCardType(n) + ' (alt text)'];
                    if (altText !== '' &amp;&amp; altText !== undefined) {
                        card.attr('alt', altText);
                    }

                    serializedCard = { revealed: false, type: "image", content: pairs[j][presenter.numberToCardType(n) + ' (image)'] }
                }

                card.addClass('card').attr({'card_id' : j, 'card_style' : n});
                cards.push(card);

                serializedCard.cardStyle = n;
                serializedCard.cardId = j;
                serializedCards.push(serializedCard);
            }
        }

        presenter.cards = cards;
        presenter.serializedCards = serializedCards;

        presenter.shuffleCards();
    };

    presenter.shuffleCards = function() {
        var shuffled = presenter.shuffleTwoArrays(presenter.cards, presenter.serializedCards);

        presenter.cards = shuffled[0];
        presenter.serializedCards = shuffled[1];
    };

    presenter.prepareGridFromSavedState = function(savedCards) {
        var cards = [], card,
            pairs = presenter.model['Pairs'], src;

        for(var i = 0; i &lt; savedCards.length; i++) {
            if(savedCards[i].type == "text") {
                card = $('&lt;p&gt;&lt;/p&gt;').text(savedCards[i].content);
            } else {
                src = pairs[savedCards[i].cardId][presenter.numberToCardType(savedCards[i].cardStyle) + ' (image)'];
                card = $('&lt;img/&gt;').attr({ src: src });

                var altText = pairs[savedCards[i].cardId][presenter.numberToCardType(savedCards[i].cardStyle) + ' (alt text)']
                if (altText !== undefined &amp;&amp; altText !== '') {
                    card.attr('alt', altText);
                }
            }
            card.addClass('card').attr({'card_id' : savedCards[i].cardId, 'card_style' : savedCards[i].cardStyle });
            cards.push(card);
        }

        presenter.cards = cards;
        presenter.serializedCards = savedCards;
    };

    presenter.handleCardClickedFirst = function(card) {
        presenter.state = presenter.STATES.CLICKED_FIRST;
        if(presenter.imageMode == 'KeepAspect'){
            if(card.hasClass('cell')){
                presenter.cardClickedFirst = card;
            }else{
                presenter.cardClickedFirst = card.parent();
            }
        }else{
            presenter.cardClickedFirst = card;
        }
        presenter.showCard(presenter.cardClickedFirst);

        if(presenter.useTwoStyles) {
            presenter.cardClickedStyle = presenter.numberToCardType(parseInt(presenter.cardClickedFirst.find('.card').attr('card_style')));
            presenter.viewContainer.find("div.front_" + presenter.cardClickedStyle).css('cursor', 'default');
        }

        if (keyboardController) {
            var TextVoices = [];
            TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.revealed));
            TextVoices = TextVoices.concat(presenter.getCellTextVoices(keyboardController.keyboardNavigationCurrentElementIndex, {prefix: false, color: true, value: true}));
            presenter.speak(TextVoices);
        }
    };

    presenter.cardReveal = function () {
        var cells = presenter.viewContainer.find('.cell');
        presenter.serializedCards[$.inArray(presenter.cardClickedFirst[0], cells)].revealed = true;
        presenter.serializedCards[$.inArray(presenter.cardClickedSecond[0], cells)].revealed = true;
    };

    presenter.addScoreAndSentEvent = function () {
        presenter.cardReveal();

        presenter.score++;

        if (presenter.isAllOK()) {
            removeTimers();
            var now = new Date();
            presenter.spendTime += now - presenter.sessionStarted;
            presenter.sessionStarted = null;
            if (presenter.isActivity) {
                presenter.sendAllOKEvent();
            }
        }
    };

    presenter.createBaseEventData = function () {
        return {
            source: presenter.ID ,
            item: "",
            value: "",
            score: ""
        };
    };

    presenter.createItemEventData = function (firstID, secondID, isCorrect) {
        var eventData = presenter.createBaseEventData();
        var firstId = parseInt(firstID)+1;
        var secondId = parseInt(secondID)+1;

        eventData.item = firstId+"-"+secondId;
        eventData.value = '1';
        eventData.score = isCorrect ? "1" : "0";

        if (!presenter.isActivity) {
            eventData.score = "";
        }

        return eventData;
    };

    presenter.sendEventData = function (eventData) {
        eventBus.sendEvent('ValueChanged', eventData);
    };

    function numberToCardType(element) {
        if (element.is("img")) {
            return element.parent().parent().find('.card').attr('card_style');
        } else {
            return element.parent().find('.card').attr('card_style');
        }
    }

    presenter.onCardClicked = function(e) {
        e.stopPropagation();

        if (presenter.isShowAnswersActive) {
            return;
        }

        if (presenter.sessionStarted === null) {
            presenter.startSession();
        }

        var eventData;
        var cardId = $(e.target).parent().find('.card').attr('card_id');

        if(presenter.useTwoStyles) {
            var clickedStyle;
            clickedStyle = presenter.numberToCardType(parseInt(numberToCardType($(e.target))));
            if(clickedStyle == presenter.cardClickedStyle) {
                presenter.speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.wrongColor)]);
                return;
            }
        }

        if(presenter.configuration.clickToTurnOverIncorrectPair &amp;&amp; presenter.STATES.CLICKED_SECOND) {

        }

        switch(presenter.state) {
            case presenter.STATES.READY:
                presenter.handleCardClickedFirst($(e.target).parent());
                break;

            case presenter.STATES.CLICKED_FIRST:
                presenter.state = presenter.STATES.CLICKED_SECOND;

                if(presenter.imageMode == 'KeepAspect'){
                    if($(e.target).parent().hasClass('cell')){
                        presenter.cardClickedSecond = $(e.target).parent();
                    }else{
                        presenter.cardClickedSecond = $(e.target).parent().parent();
                    }
                }else{
                    presenter.cardClickedSecond = $(e.target).parent();
                }

                presenter.showCard(presenter.cardClickedSecond);

                presenter.cardClickedFirstId = presenter.cardClickedFirst.find('.card').attr('card_id');
                presenter.cardClickedSecondId = presenter.cardClickedSecond.find('.card').attr('card_id');

                var firstIndex = presenter.getCardIndex(presenter.cardClickedFirst);

                var TextVoices = [];
                TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.revealed));
                TextVoices = TextVoices.concat(presenter.getCellTextVoices(keyboardController.keyboardNavigationCurrentElementIndex, {prefix: false, color: true, value: true}));

                if(presenter.cardClickedFirstId != presenter.cardClickedSecondId) {
                    presenter.errorCount++;
                    presenter.markCardMismatch(presenter.cardClickedFirst.find(".card"), presenter.cardClickedFirst.find(".card"));
                    presenter.markCardMismatch(presenter.cardClickedSecond.find(".card"), presenter.cardClickedFirst.find(".card"));

                    eventData = presenter.createItemEventData(presenter.cardClickedFirstId, presenter.cardClickedSecondId,  false);
                    presenter.sendEventData(eventData);
                    presenter.isCorrectMark = false;
                    TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.notMatch));
                } else {
                    presenter.markCardTick(presenter.cardClickedFirst.find(".card"), presenter.cardClickedFirst.find(".card"));
                    presenter.markCardTick(presenter.cardClickedSecond.find(".card"), presenter.cardClickedFirst.find(".card"));

                    eventData = presenter.createItemEventData(presenter.cardClickedFirstId, presenter.cardClickedSecondId, true);
                    presenter.sendEventData(eventData);

                    presenter.addScoreAndSentEvent();
                    presenter.isCorrectMark = true;
                    TextVoices.push(window.TTSUtils.getTextVoiceObject(presenter.speechTexts.match));

                    presenter.cardClickedFirst = null;
                    presenter.cardClickedSecond = null;
                }
                TextVoices = TextVoices.concat(presenter.getCellTextVoices(firstIndex, {prefix: false, color: true, value: true}));
                presenter.speak(TextVoices);

                if(presenter.useTwoStyles) {
                    presenter.viewContainer.find("div.front_" + presenter.cardClickedStyle).css('cursor', '');
                    presenter.cardClickedStyle = null;
                }

                break;

            case presenter.STATES.CLICKED_SECOND:
                if(presenter.cardClickedFirstId != presenter.cardClickedSecondId) {
                    presenter.hideCard(presenter.cardClickedFirst);
                    presenter.hideCard(presenter.cardClickedSecond);
                }

                if(presenter.configuration.clickToTurnOverIncorrectPair &amp;&amp; !presenter.isCorrectMark) {
                    presenter.cardClickedFirst = null;
                    presenter.state = presenter.STATES.READY;
                    presenter.speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.turnOver)]);
                } else {
                    presenter.handleCardClickedFirst($(e.target).parent());
                }

                presenter.cardClickedSecond = null;
                presenter.cardClickedFirstId = null;
                presenter.cardClickedSecondId = null;

                if (presenter.keppWrongMarking) {
                    var mark = presenter.viewContainer.find('.mismatch_mark');
                    fadeOutMark(mark, 1000);
                }

                break;
        }
    };

    presenter.onBackClick = function () {
        if(presenter.STATES.CLICKED_SECOND &amp;&amp; presenter.configuration.clickToTurnOverIncorrectPair &amp;&amp; !presenter.isCorrectMark) {
            if(presenter.cardClickedFirstId != presenter.cardClickedSecondId) {
                presenter.hideCard(presenter.cardClickedFirst);
                presenter.hideCard(presenter.cardClickedSecond);
            }

            presenter.cardClickedFirst = null;
            presenter.state = presenter.STATES.READY;
            presenter.cardClickedSecond = null;
            presenter.cardClickedFirstId = null;
            presenter.cardClickedSecondId = null;

            if (presenter.keppWrongMarking) {
                var mark = presenter.viewContainer.find('.mismatch_mark');
                fadeOutMark(mark, 1000);
            }
        }
    };

    presenter.createGrid = function() {
        var cards = presenter.cards;
        var keyboardNavigationElements = [];

        var $container = $('&lt;div class="gamememo_container"&gt;&lt;/div&gt;');

        if(presenter.keepAspectRatio) {
            if(presenter.requestedRowHeight &gt; presenter.requestedColumnWidth) {
                $container.css('height', (presenter.requestedColumnWidth * presenter.rowCount) + 'px');
            } else if (presenter.requestedColumnWidth &gt;= presenter.requestedRowHeight) {
                $container.css('width', (presenter.requestedRowHeight * presenter.columnCount) + 'px');
                $container.css('height', (presenter.requestedRowHeight * presenter.rowCount) + 'px');
            }
        } else {
            $container.css({
                width: presenter.viewContainer.css('width'),
                height: presenter.viewContainer.css('height')
            });
        }

        var columnWidthPercent = (100.0 / presenter.columnCount) + '%';
        var rowHeightPercent = (100.0 / presenter.rowCount) + '%';

        for(var r = 0; r &lt; presenter.rowCount; r++) {

            for(var c = 0; c &lt; presenter.columnCount; c++) {
                var front = $('&lt;div class="front placeholder"&gt;&lt;/div&gt;')
                    .addClass(presenter.useTwoStyles ?
                    'front_' + presenter.numberToCardType(parseInt(cards[r * presenter.columnCount + c].attr('card_style'))) :
                    'front_A');

                if(!presenter.preview) {
                    front.click(presenter.onCardClicked);
                }

                var back = $('&lt;div class="back placeholder"&gt;&lt;/div&gt;')
                    .append(cards[r * presenter.columnCount + c]);

                var cell = $('&lt;div class="cell"&gt;&lt;/div&gt;').css({
                    height : rowHeightPercent,
                    width : columnWidthPercent
                });

                if (presenter.configuration.isTabindexEnabled) {
                    cell.attr("tabindex", "0");
                }

                if(!presenter.preview) {
                    cell.append(back).append(front);
                    back.click(presenter.onBackClick);
                } else {
                    if(presenter.previewCards) {
                        back.css('display', 'block');
                        cell.append(back);

                    } else {
                        cell.append(back).append(front);
                    }
                }

                $container.append(cell);
                keyboardNavigationElements.push(cell);
            }
        }
        if (keyboardController !== null) {
            keyboardController.reload(keyboardNavigationElements, presenter.columnCount);
        } else {
            keyboardController = new MemoKeyboardController(keyboardNavigationElements, presenter.columnCount);
        }

        if(presenter.styleAImage != null){
            var frontDivA = $container.find('div.front_A');
            presenter.setDivImage(frontDivA, presenter.styleAImage, presenter.configuration.altTextStyleA);
        }

        if(presenter.styleBImage != null){
            var frontDivB = $container.find('div.front_B');
            presenter.setDivImage(frontDivB, presenter.styleBImage, presenter.configuration.altTextStyleB);
        }

        presenter.viewContainer.children('div').append($container);

        $container.find('p.card').each(function(i, e) {
            var element = $(e);
            element.css({
                position : 'absolute',
                width : '100%',
                top : Math.round((parseInt(element.parent().css('height')) - parseInt(element.css('height'))) / 2) + 'px'
            });
        });

        if(!presenter.preview || (presenter.preview &amp;&amp; !presenter.previewCards))
            $container.find('div.back').css('visibility', 'hidden');

        if(presenter.imageMode == 'Stretch'){
            presenter.viewContainer.find('.gamememo_container .cell .back.placeholder img').css({
                'width': '100%',
                'height': '100%'
            });
        }

        if(presenter.imageMode == 'KeepAspect'){
            presenter.viewContainer.find('.gamememo_container .cell .back.placeholder img').css({
                'display': 'block',
                'max-width': presenter.requestedColumnWidth,
                'max-height': presenter.requestedRowHeight,
                'width': 'auto',
                'height': 'auto'
            });

            presenter.viewContainer.find('.gamememo_container .cell .back.placeholder img').load(function () {
                presenter.viewContainer.find('.gamememo_container .cell .back.placeholder').each(function () {
                    centerImage(this);
                });
            });

            presenter.viewContainer.find('img').load(function () {
                presenter.viewContainer.find('div.front_A').each(function () {
                    centerImage(this);
                });
            });

            presenter.viewContainer.find('img').load(function () {
                presenter.viewContainer.find('div.front_B').each(function () {
                    centerImage(this);
                });
            });
        }
    };

    presenter.setDivImage = function($div, image, altText) {
        var encodedURI = encodeURI(image);

        if (presenter.imageMode == 'Stretch') {
            $div.css({
                'background': 'url(' + encodedURI + ')',
                'background-size': '100% 100%'
            });
        } else if(presenter.imageMode == 'KeepAspect') {
            var img = $('&lt;img&gt;');
            img.attr('src', encodedURI);
            img.css({
                'display': 'block',
                'max-width': presenter.requestedColumnWidth,
                'max-height': presenter.requestedRowHeight,
                'width': 'auto',
                'height': 'auto'
            });
            $div.append(img);
            $div.css('background', 'transparent');
        } else {
            $div.css({
                'background': 'url(' + encodedURI + ')'
            });
        }

        if (altText !== undefined) {
            var altTextSpan = document.createElement('span');
            altTextSpan.innerText = altText;
            altTextSpan.className = 'gamememo_alt_text';
            $div.append(altTextSpan);
        }
    };

    function centerImage(element) {
        var imgTop = ($(element).height() - $(element).find('img').height())/2;
        var imgLeft = ($(element).width() - $(element).find('img').width())/2;

        $(element).find('img').css({
            'top': imgTop+'px',
            'left': imgLeft+'px',
            'position': 'relative'
        });
    }

    presenter.initializeLogic = function(view, model) {
        presenter.viewContainer = $(view);
        model = presenter.upgradeModel(model);
        presenter.model = model;

        presenter.configuration = presenter.validateModel(model);
        presenter.ID = model.ID;
        if(presenter.configuration.isError) {
            presenter.showErrorMessage(presenter.configuration.errorMessage, presenter.configuration.errorMessageSubstitutions);
        } else {
            presenter.prepareGrid();
            presenter.createGrid();
        }

    };

    presenter.destroy = function () {
        removeTimers();
    };

    presenter.run = function(view, model) {
        presenter.preview = false;
        presenter.initializeLogic(view, model);
        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            eventBus.addEventListener(events[i], this);
        }
        presenter.setVisibility(presenter.configuration.isVisible);
        MutationObserverService.createDestroyObserver(presenter.ID, presenter.destroy, presenter.viewContainer.get(0));
        MutationObserverService.setObserver();
    };

    presenter.createPreview = function(view, model) {
        presenter.preview = true;
        presenter.initializeLogic(view, model);
    };

    var sendSessionEvent = function (eventType) {
        var eventData = {
            'source': presenter.ID,
            'item': eventType,
            'value': '',
            'score': ''
        };
        eventBus.sendEvent('ItemSelected', eventData);
    };

    presenter.startSession = function () {
        if (presenter.timeToSolve &gt; 0) {
            presenter.sessionStarted = new Date();
            var timeout = presenter.timeToSolve*1000 - presenter.spendTime;
            if (timeout &lt;= 0 ){
                presenter.setLockScreen();
            } else {
                presenter.timer = setTimeout(presenter.setLockScreen, timeout);
                presenter.timeInterval = setInterval(presenter.sendTimerEvent, 1000);
                sendSessionEvent('startSession');
            }
        }
    };

     function formatTime(seconds) {
        var minutes = Math.floor(seconds / 60);
        minutes = (minutes &gt;= 10) ? minutes : "0" + minutes;
        seconds = Math.floor(seconds % 60);
        seconds = (seconds &gt;= 10) ? seconds : "0" + seconds;
        return minutes + ":" + seconds;
    }

    presenter.sendTimerEvent = function AddonMemo_sendTimerEvent() {
        var now = new Date(),
        spendTime = Math.round(((now - presenter.sessionStarted) + presenter.spendTime) / 1000),
        remainingTime = presenter.timeToSolve - spendTime,
        progress = Math.round(100 * spendTime / presenter.timeToSolve),
        eventData = {
            'source': presenter.ID,
            'item': progress,
            'value': formatTime(spendTime),
        };
        eventBus.sendEvent('ItemConsumed', eventData);
    };

    function removeTimers() {
        if (presenter.timeInterval){
            clearInterval(presenter.timeInterval);
        }
        if (presenter.timer){
            clearTimeout(presenter.timer);
        }
    };

    presenter.setLockScreen = function AddonMemo_setLockScreen() {
        presenter.turnOffUserInteraction();
        var gamememo_container = presenter.viewContainer.find('.gamememo_container');
        var lockScreen = $('&lt;div class="memo-lock-screen"&gt;&lt;/div&gt;'),
            lockScreenInfo = $('&lt;div class="memo-lock-screen-info"&gt;&lt;/div&gt;');

        var sessionEndedMessage = presenter.sessionEndedMessage;
        if (textParser) {
            sessionEndedMessage = textParser.parse(sessionEndedMessage);
        }
        lockScreenInfo.html(sessionEndedMessage);
        gamememo_container.append(lockScreen);
        gamememo_container.append(lockScreenInfo);
        sendSessionEvent('endSession');
        removeTimers();
        var TextVoices = window.TTSUtils.getTextVoiceArrayFromElement(lockScreenInfo,presenter.configuration.langTag);
        presenter.speak(TextVoices);
        screenLocked = true;
    };

    presenter.removeLockScreen = function AddonMemo_removeLockScreen() {
        var gamememo_container = presenter.viewContainer.find('.gamememo_container');
        gamememo_container.find('.memo-lock-screen').remove();
        gamememo_container.find('.memo-lock-screen-info').remove();
        screenLocked = false;
    };

    presenter.getState = function() {
        var state ={
            score: presenter.score,
            errorCount: presenter.errorCount,
            cards: presenter.serializedCards,
            isVisible: presenter.configuration.isVisible
        };
        if (presenter.timeToSolve &gt; 0) {
            var spendTime = presenter.spendTime;
            if (presenter.sessionStarted !== null) {
                var now = new Date();
                spendTime += now - presenter.sessionStarted
            }
            state['spendTime'] = spendTime;
        }
        return JSON.stringify(state);
    };

    presenter.setState = function(state) {
        var stateObj = JSON.parse(state);

        if (stateObj.spendTime) {
            presenter.spendTime = stateObj.spendTime;
        }
        presenter.viewContainer.html('&lt;div&gt;&lt;/div&gt;');
        presenter.score = stateObj.score;
        presenter.errorCount = stateObj.errorCount;
        presenter.state = presenter.STATES.READY;

        presenter.prepareGridFromSavedState(stateObj.cards);
        presenter.createGrid();

        var cell;

        for(var i = 0; i &lt; stateObj.cards.length; i++) {
            if(stateObj.cards[i].revealed) {
                cell = $(presenter.viewContainer.find(".cell")[i]);

                cell.children(".front").css('visibility', 'hidden');
                cell.children(".back").css('visibility', 'visible');
                cell.addClass("was-clicked");
            }
        }

        if (stateObj.isVisible != undefined) {
            presenter.configuration.isVisible = stateObj.isVisible;
            presenter.setVisibility(presenter.configuration.isVisible);
        }

        if (presenter.timeToSolve &amp;&amp; presenter.spendTime &amp;&amp;
            (presenter.timeToSolve*1000 &lt;= presenter.spendTime)){
                presenter.setLockScreen();
        }
    };

    presenter.concealAllCards = function () {
        $.each(presenter.serializedCards, function (index, value) {
            value.revealed = false;
        });
    };


    presenter.reset = function(){
        presenter.viewContainer.html('&lt;div&gt;&lt;/div&gt;');
        presenter.score = 0;
        presenter.errorCount = 0;
        presenter.state = presenter.STATES.READY;

        presenter.cardClickedFirst = null;
        presenter.cardClickedSecond = null;
        presenter.cardClickedFirstId = null;
        presenter.cardClickedSecondId = null;
        presenter.areClickedCardsHiddenInGSAMode = false;

        presenter.isShowErrorsMode = false;

        presenter.prepareGrid();
        presenter.createGrid();
        presenter.concealAllCards();

        const args = [];
        args.push("Typeset", MathJax.Hub, presenter.viewContainer.find(".gamememo_container")[0]);
        MathJax.Hub.Queue(args);

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisibleByDefault);
        if (presenter.timeToSolve) {
            presenter.spendTime = 0;
            presenter.sessionStarted = null;
            presenter.removeLockScreen();
            removeTimers()
        }
        if (keyboardController &amp;&amp; keyboardController.keyboardNavigationActive) {
            keyboardController.resetPosition();
        }
    };

    presenter.getErrorCount = function() {
        if (!presenter.isActivity) {
            return 0;
        }

        var lastErrorCount = presenter.errorCount;
        presenter.errorCount = 0;

        return lastErrorCount;
    };

    presenter.getMaxScore = function() {
        return presenter.isActivity ? presenter.maxScore : 0;
    };

    presenter.getScore = function() {
        return presenter.isActivity ? presenter.score : 0;
    };

    presenter.executeCommand = function (name, params) {
        if (presenter.configuration.isErrorMode) return;

        var commands = {
            'isAllOK': presenter.isAllOK,
            'showAnswers' : presenter.showAnswers,
            'hideAnswers' : presenter.hideAnswers,
            'show': presenter.show,
            'hide': presenter.hide
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore();
    };

     presenter.sendAllOKEvent = function () {
        var eventData = {
            'source': presenter.ID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.setShowErrorsMode = function(){
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (presenter.isActivity) {
            presenter.turnOffUserInteraction();

            markCardsWithCorrectWrongStyle(filterClickedCards(getClickedCards()), true);
        }
        presenter.isShowErrorsMode = true;
    };

    function getClickedCards() {
        return filterClickedCards(presenter.viewContainer.find('.cell.was-clicked'));
    }

    function filterClickedCards(clickedCards) {
        var filteredArray = [];
        for(var i = 0; i &lt; clickedCards.length; i++) {
            filteredArray.push(clickedCards[i]);
        }

        return filteredArray;
    }

    function filterCardsById (cardsArray, id) {
        return cardsArray.filter(function (element) {
                return ($(element).find("[card_id]").attr("card_id") ==  id);
        });
    }

    function getStyleAction (isAdding) {
        if (isAdding) {
            return function (element, style) {
                $(element).addClass(style);
            };
        }

        return function (element, style) {
            $(element).removeClass(style);
        };
    }

    function markCardsWithCorrectWrongStyle (clickedCards, isAdding) {
        var styleAction = getStyleAction(isAdding);


        function styleWithCorrect(element) {
            styleAction(element, "cell-correct");
        }

        function styleWithWrong(element) {
            styleAction(element, "cell-wrong");
        }

        var usedIds = [];

        clickedCards.map(function (element) {
            var id = $(element).find("[card_id]").attr("card_id");

            if (usedIds.indexOf(id) == -1) {

                var cardsById = filterCardsById(clickedCards, id);

                if (cardsById.length == 2) {
                    cardsById.map(styleWithCorrect);
                } else {
                    cardsById.map(styleWithWrong);
                }
                usedIds.push(id);
            }
        });
    }

    function getClickablePartOfCards() {
        return presenter.viewContainer.find(".front.placeholder");
    }

    function unbindOnCardsCollection (cardsCollection) {
        for(var i = 0; i &lt; cardsCollection.length; i++) {
            $(cardsCollection[i]).unbind();
        }
    }

    function bindClickInteractionOnCardsCollection (cardsCollection) {
        for(var i = 0; i &lt; cardsCollection.length; i++) {
            $(cardsCollection[i]).click(presenter.onCardClicked);
        }
    }

    presenter.turnOffUserInteraction = function () {
        keyboardController.selectEnabled(false);
        unbindOnCardsCollection(getClickablePartOfCards());
    };

    presenter.turnOnUserInteraction = function () {
        keyboardController.selectEnabled(true);
        bindClickInteractionOnCardsCollection(getClickablePartOfCards());
    };

    presenter.setWorkMode = function () {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        if (presenter.isActivity) {
            presenter.turnOnUserInteraction();
            markCardsWithCorrectWrongStyle(getClickedCards(), false);
        }
        presenter.isShowErrorsMode = false;
    };

    presenter.onEventReceived = function (eventName, data) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        }

        if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        }

        if (eventName == "GradualShowAnswers") {
            if (presenter.configuration.ID == data.moduleID) {
                presenter.gradualShowAnswers();
            } else {
                presenter.activitiesCount = presenter.getActivitiesCount();
                presenter.isShowAnswersActive = true;
            }
        }

        if (eventName == "GradualHideAnswers") {
            presenter.hideAnswers();
        }
    };

    presenter.showAnswers = function () {
        if (!presenter.isActivity) {
            return;
        }

        presenter.isShowAnswersActive = true;
        presenter.isShowErrorsMode = false;

        presenter.showCard(presenter.viewContainer.find('.cell'));
        presenter.viewContainer.find('.cell').addClass('cell-show-answers');
        keyboardController.selectEnabled(false);
    };

    presenter.getIfUnexposedCardInGSAMode = function(currentCard) {
        if (!currentCard.hasClass('was-clicked') &amp;&amp; !currentCard.hasClass('exposed-in-GSA-mode')) {
            return currentCard;
        }
        return undefined;
    }

    presenter.showInGSAModeIfCardsMatch = function(firstCard, secondCard) {
        firstCardId = firstCard.find('.card').attr('card_id');
        secondCardId = secondCard.find('.card').attr('card_id');
        if (firstCardId === secondCardId) {
            presenter.showCard(firstCard);
            presenter.showCard(secondCard);
            firstCard.addClass('exposed-in-GSA-mode');
            secondCard.addClass('exposed-in-GSA-mode');
            return true;
        }
        return false;
    }

    presenter.gradualShowAnswers = function () {
        if (!presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            if (!presenter.isActivity) {
                return;
            }

            presenter.hideClickedCardsInGSAMode();

            presenter.isShowAnswersActive = true;
            presenter.isShowErrorsMode = false;

            var currentCard = presenter.viewContainer.find('.cell').first();
            const numberOfCards = presenter.viewContainer.find('.cell').length;
            var firstCard;
            for (var i = 0; i &lt; numberOfCards; i++) {
                if (firstCard === undefined) {
                    firstCard = presenter.getIfUnexposedCardInGSAMode(currentCard);
                } else {
                    var matched = presenter.showInGSAModeIfCardsMatch(firstCard, currentCard);
                    if (matched) {
                        break;
                    }
                }
                currentCard = currentCard.next();
            }
            keyboardController.selectEnabled(false);
        } else {
            presenter.showAnswers();
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.isActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.showClickedCardsInGSAMode();

        presenter.viewContainer.find('.cell').removeClass('cell-show-answers');
        presenter.viewContainer.find('.cell').removeClass('exposed-in-GSA-mode');
        presenter.viewContainer.find('.cell').find('.back').css('visibility', 'hidden');
        presenter.viewContainer.find('.cell').find('.front').css('visibility', 'visible');

        presenter.viewContainer.find('.was-clicked').find('.front').css('visibility', 'hidden');
        presenter.viewContainer.find('.was-clicked').find('.back').css('visibility', 'visible');

        presenter.isShowAnswersActive = false;
        keyboardController.selectEnabled(true);
    };

    presenter.getActivitiesCount = function () {
        if (presenter.configuration.showAllAnswersInGradualShowAnswersMode) {
            return 1;
        }
        if (!presenter.isShowAnswersActive) {
            const numberOfCards = presenter.viewContainer.find('.cell').length;
            var cardsLeftCounter = 0;
            var currentCard = presenter.viewContainer.find('.cell').first();
            for (i = 0; i &lt; numberOfCards; i++) {
                currentCardIsClicked = ((presenter.cardClickedFirst &amp;&amp; currentCard[0] === presenter.cardClickedFirst[0])
                                     || (presenter.cardClickedSecond &amp;&amp; currentCard[0] === presenter.cardClickedSecond[0]))
                if (!currentCard.hasClass('was-clicked') || currentCardIsClicked) {
                    cardsLeftCounter++;
                }
                currentCard = currentCard.next();
            }
            presenter.activitiesCount = cardsLeftCounter / 2;
        }
        return presenter.activitiesCount;
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
    };

    presenter.setVisibility = function(isVisible) {
        presenter.viewContainer.css("visibility", isVisible ? "visible" : "hidden");
        presenter.viewContainer.css("display", isVisible ? "block" : "none");
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        keyboardController.handle(keycode, isShiftKeyDown, event)
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.speak = function(data) {
      var tts = presenter.getTextToSpeechOrNull(playerController);

        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    };


    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="graph" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
        <property displayName="Is not an activity" name="isNotActivity" nameLabel="graph_property_is_not_activity" type="boolean"/>
		<property isLocalized="true" name="X axis description" nameLabel="graph_property_x_axis_description" type="string"/>
		<property name="Show X axis bars descriptions" nameLabel="graph_property_show_x_axis_bars_descriptions" type="boolean"/>
		<property name="X axis bars descriptions" nameLabel="graph_property_x_axis_bars_descriptions" type="list">
			<property isLocalized="true" name="Description" nameLabel="graph_property_description" type="string"/>
		</property>
		<property name="Show X axis series descriptions" nameLabel="graph_property_show_x_axis_series_descriptions" type="boolean"/>
		<property name="X axis series descriptions" nameLabel="graph_property_x_axis_series_descriptions" type="list">
			<property isLocalized="true" name="Description" nameLabel="graph_property_description" type="string"/>
		</property>
		<property isLocalized="true" name="Y axis description" nameLabel="graph_property_y_axis_description" type="string"/>
		<property name="Y axis maximum value" nameLabel="graph_property_y_axis_maximum_value" type="string"/>
		<property name="Y axis minimum value" nameLabel="graph_property_y_axis_minimum_value" type="string"/>
		<property name="Y axis grid step" nameLabel="graph_property_y_axis_grid_step" type="string"/>
        <property name="Y axis values" nameLabel="graph_property_y_axis_values" type="string"/>
		<property name="Interactive" nameLabel="graph_property_interactive" type="boolean"/>
		<property name="Interactive step" nameLabel="graph_property_interactive_step" type="string"/>
		<property name="Data" nameLabel="graph_property_data" type="text"/>
		<property name="Answers" nameLabel="graph_property_answers" type="list">
			<property name="Answer" nameLabel="graph_property_answer" type="string"/>
			<property name="Example" nameLabel="graph_property_is_example" type="boolean"/>
		</property>
		<property name="Series colors" nameLabel="graph_property_series_colors" type="list">
			<property name="Color" nameLabel="graph_property_color" type="string"/>
		</property>
		<property name="Decimal separator" nameLabel="graph_property_decimal_separator" type="string"/>
	</model>
<css>.graph_container_outer {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}

.graph_container_outer div {
    position: absolute;
}

.graph_container_inner {
    top: 10px;
    left: 10px;
    bottom: 10px;
    right: 10px;
    -ms-touch-action: none;
}

.graph_axis_y_description {
    top: 0;
    left: 0;
}

.graph_axis_x_description {
    right: 0;
}

.graph_chart_outer {
    left: 0;
    bottom: 0;
}

.graph_grid_description {
    text-align: right;
    color: #999;
    font-size: 75%;
}

.graph_chart_inner {
    top: 0;
    bottom: 0;
    right: 0;
}

.graph_axis_line {
    background: #000;
}

.graph_axis_x_line {
    left: 0;
    height: 2px;
    right: 0;
}

.graph_axis_y_line {
    left: 0;
    width: 2px;
    bottom: 0;
    top: 0;
}

.graph_axis_arrow {
    width: 0;
    height: 0;
    position: absolute;
}

.graph_axis_y_arrow {
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    border-bottom: 5px solid black;
    top: 0;
    left: -4px;
}

.graph_axis_x_arrow {
    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    border-left: 5px solid black;
    right: 0;
}

.graph_grid_block {
    left: 0;
    right: 0;
}

.graph_grid_block_above {
    border-top: 1px dotted #999;
}

.graph_grid_block_below {
    border-bottom: 1px dotted #999;
}

.graph_serie_container {
    position: relative  !important;
    float: left;
    height: 100%;
}

.graph_serie_element {
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}

.graph_column_container {
    position: relative !important;
    float: left;
    height: 100%;
}

.graph_value_container {
    width: 100%;
    bottom: 0;
}

.graph_value_element {
    border-left: 1px solid #666;
    border-right: 1px solid #666;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}


.graph_value_element_positive {
    border-top: 1px solid #666;
}

.graph_value_element_negative {
    border-bottom: 1px solid #666;
}

.graph_value_element_invalid {
    border-left-color: #ff0000;
    border-right-color: #ff0000;
}

.graph_value_element_positive_invalid {
    border-top-color: #ff0000;
}

.graph_value_element_negative_invalid {
    border-bottom-color: #ff0000;
}

.graph_value_element_valid {
    border-left-color: #00ff00;
    border-right-color: #00ff00;
}

.graph_value_element_positive_valid {
    border-top-color: #00ff00;
}

.graph_value_element_negative_valid {
    border-bottom-color: #00ff00;
}


.graph_grid, .graph_series, .graph_axises  {
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}

.graph_column_container_interactive {
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
}

.graph_column_container_above {
    cursor: n-resize;
}

.graph_value_element_interactive, .graph_column_container_below {
    cursor: s-resize;
}

.graph_serie_description, .graph_column_description {
    text-align: center;
    color: #999;
    font-size: 75%;
    overflow: hidden;
    height: 16px;
    left: 0;
    right: 0;
}

.graph_column_description {
    bottom: -20px;
}
</css><view/><preview/><presenter>function Addongraph_create(){

    /*
    *KNOWN ISSUES:
    *       Invalid properties values (Data, Y grid step, Y max, Y min values):
    *           addon graph uses functions with errors - parseInt &amp; parseFloat. It creates situations where user could
    *           type in properties fields digits with strings, and graph still shows up as valid. Changing validation to
    *           be too much accurate can break backward compatibility
    *
    *       Answers greater than Y max / lower than Y min:
    *           Answers validation is invalid. It allows create graphs where answer is greater or lower than YMax/YMin.
    *           Changing this validation can break backward compatibility, graph addon have to allow such invalid situations.
    *
    *       Y grid step greater than Y max:
    *           Y grid step validation is also invalid. Allows situations where grid is greater then Y max, it should
    *           just show nothing, no grid &amp; no Y axis values.
    *
    *       Model validation flow:
    *          Logic of parsing &amp; validating properties requires some properties be checked and parsed at first.
    *          Don't changeflow of model validation.
    *
    */
    var presenter = function(){};

    presenter.drawingXPosition  = null;
    presenter.absoluteRange     = null;
    presenter.chartInner        = null;
    presenter.axisXLine         = null;
    presenter.eventBus          = null;
    presenter.playerController  = null;
    presenter.errorMode         = false;
    presenter.isStarted         = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.GSAcounter        = 0;
    presenter.currentData       = [];

    presenter.$view             = null;
    presenter.configuration     = {};
    var minimumValueGraph;


    presenter.ERROR_MESSAGES = {
        DATA_ROW_NOT_ENOUGH_COLUMNS:      "Row %row% in data contains not enough columns, minimum amount of columns is 2 - first indicates X axis description, second and further contain values",
        DATA_ROW_MALFORMED:               "Row %row% is not valid CSV - check its syntax",
        DATA_ROW_VALUE_NOT_NUMERIC:       "Value \"%value%\" of column %column% of row %row% is not numeric",
        DATA_ROW_DIFFERENT_COLUMNS_COUNT: "Row %row% contains different amount of columns than previous rows",
        AXIS_Y_MAXIMUM_VALUE_NOT_NUMERIC: "Y axis maximum value is not numeric",
        AXIS_Y_MINIMUM_VALUE_NOT_NUMERIC: "Y axis minimum value is not numeric",
        AXIS_Y_MAXIMUM_VALUE_TOO_SMALL:   "Cannot fit graph into view container - Y axis maximum value of %range% is smaller than maximum value %value% passed with the data",
        AXIS_Y_MINIMUM_VALUE_TOO_BIG:     "Cannot fit graph into view container - Y axis minimum value of %range% is bigger than minimum value %value% passed with the data",
        AXIS_Y_DOES_NOT_INCLUDE_ZERO:     "Invalid Y axis minimum &amp; maximum value - graph should contain value of zero",
        AXIS_Y_GRID_STEP_NOT_NUMERIC:     "Y axis grid step is not numeric",
        SERIES_COLORS_AMOUNT_INVALID:     "Amount of Series colors is different that amount of columns in the data",
        INTERACTIVE_STEP_NOT_NUMERIC:     "Interactive step is not numeric",
        INTERACTIVE_STEP_NOT_POSITIVE:    "Interactive step is not a positive integer",
        ANSWER_NOT_NUMERIC:               "Answer \"%answer%\" is not numeric",
        ANSWERS_AMOUNT_INVALID:           "Amount of answers (%answers%) has to be equal amount of bars (%bars%)",
        AXIS_X_SERIES_DESCRIPTIONS_AMOUNT_INVALID: "Amount of X axis series descriptions (%descriptions%) has to be equal to amount of series (%series%)",
        AXIS_X_BARS_DESCRIPTIONS_AMOUNT_INVALID:   "Amount of X axis bars descriptions (%descriptions%) has to be equal to amount of bars (%bars%)",
        YAV_01: "Y axis values have to be float numbers.",
        YAV_02: "Cyclic value can't be zero number in Y axis values property.",
        YAV_03: "Y axis values can't be greater than Y maximum value.",
        YAV_04: "Y axis values can't be lower than Y minimum value.",
        YAV_05: "Cyclic value can't be negative number in Y axis values property.",
        YAV_06: "Y axis values can't have duplicated numbers"
    };

    presenter.CSS_CLASSES = {
        SHOW_EXAMPLE : 'graph_show_example'
    };

    presenter.showErrorMessage = function(message, substitutions) {
        var errorContainer;
        if(typeof(substitutions) == 'undefined') {
            errorContainer = '&lt;p&gt;' + message + '&lt;/p&gt;';
        } else {
            var messageSubst = message;
            for(var key in substitutions) {
                messageSubst = messageSubst.replace('%' + key + '%', substitutions[key]);
            }
            errorContainer = '&lt;p&gt;' + messageSubst + '&lt;/p&gt;';
        }

        presenter.$view.html(errorContainer);
    };

    presenter.redrawGraphValue = function(valueContainer) {
        var currentValue = parseFloat(valueContainer.attr('current-value'));
        var valueElement = valueContainer.find('.graph_value_element');

        if(currentValue &gt;= 0) {
            valueElement.removeClass('graph_value_element_negative').addClass('graph_value_element_positive');
            valueContainer.css({
                bottom: (presenter.drawingXPosition - Math.round(presenter.axisXLine.height() / 2)) + 'px',
                height: parseFloat(currentValue / presenter.absoluteRange) * 100 + '%',
                top: ''
            });

        } else if (currentValue &lt; 0) {
            valueElement.removeClass('graph_value_element_positive').addClass('graph_value_element_negative');
            valueContainer.css({
                height: parseFloat(currentValue * -1 / presenter.absoluteRange) * 100 + '%',
                top: (presenter.chartInner.height() - presenter.drawingXPosition + Math.round(presenter.axisXLine.height() / 2)) + 'px',
                bottom: ''
            });
        }
    };


    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;
        presenter.eventBus = presenter.playerController.getEventBus();
    };


    presenter.areAllOk = function(currentGridValues) {
        for (var i = 0; i &lt; currentGridValues.length; i++) {
            if (currentGridValues[i].indexOf(false) !== -1) {
                return false;
            }
        }

        return true;
    };

    presenter.sendOverallScoreEvent = function(direction, valueId, newValue, wasAllValidBeforeChange, wasThisValidBeforeChange, willAllBeValidAfterChange, willThisBeValidAfterChange) {
        var score =
            (wasAllValidBeforeChange    ? 'BEFORE_ALL_VALID '  : 'BEFORE_ALL_INVALID ') +
                (wasThisValidBeforeChange   ? 'BEFORE_THIS_VALID ' : 'BEFORE_THIS_INVALID ') +
                (willAllBeValidAfterChange  ? 'AFTER_ALL_VALID '   : 'AFTER_ALL_INVALID ') +
                (willThisBeValidAfterChange ? 'AFTER_THIS_VALID'   : 'AFTER_THIS_INVALID');

        var parsedValue = '' + newValue;
        if (presenter.configuration.isDecimalSeparatorSet) {
            parsedValue = parsedValue.replace('.', presenter.configuration.decimalSeparator);
        }

        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item'  : valueId + ' ' + direction,
            'value' : parsedValue,
            'score' : score
        });

        presenter.configuration.results[valueId.split(' ')[0]][valueId.split(' ')[1]] = willThisBeValidAfterChange;

        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item': valueId,
            'value': parsedValue,
            'score': willThisBeValidAfterChange ? '1' : '0'
        });

        if (presenter.areAllOk(presenter.configuration.results)) {
            presenter.eventBus.sendEvent('ValueChanged', {
                'source': presenter.configuration.ID,
                'item': 'all',
                'value': '',
                'score': ''
            });
        }

    };

    presenter.areAllBarsValid = function() {
        var r = true;
        presenter.$view.find('.graph_value_container').each(function(i, e) {
            if(presenter.configuration.answers[i] != parseFloat($(e).attr('current-value'))) {
                r = false;
            }
        });

        return r;
    };

    presenter.setShowErrorsMode = function() {
        if( presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }

        if( presenter.isGradualShowAnswersActive){
            presenter.gradualHideAnswers();
        }

        presenter.errorMode = true;
        if (!presenter.isStarted || presenter.configuration.isNotActivity) {
            return 0;
        }

        presenter.configuration.shouldCalcScore = true;

        if(presenter.configuration.isInteractive) {
            presenter.$view.find('.graph_value_element_interactive, .graph_column_container_below, .graph_column_container_above').css('cursor', 'default');

            presenter.$view.find('.graph_value_container').each(function(index, element) {
                if (!presenter.configuration.exampleAnswers[index]) {
                    presenter.handleCheckingAnswers(index, element);
                }
            });
        }
    };

    presenter.handleCheckingAnswers = function (index, element) {
        if (presenter.configuration.answers[index] != parseFloat($(element).attr('current-value'))) {
            $(element).find('.graph_value_element').addClass('graph_value_element_invalid');
            $(element).find('.graph_value_element_positive').addClass('graph_value_element_positive_invalid');
            $(element).find('.graph_value_element_negative').addClass('graph_value_element_negative_invalid');
        } else {
            $(element).find('.graph_value_element').addClass('graph_value_element_valid');
            $(element).find('.graph_value_element_positive').addClass('graph_value_element_positive_valid');
            $(element).find('.graph_value_element_negative').addClass('graph_value_element_negative_valid');
        }
    };

    presenter.setWorkMode = function() {
        presenter.errorMode = false;
        presenter.configuration.shouldCalcScore = true;

        if(presenter.configuration.isInteractive) {
            presenter.$view.find('.graph_value_element_interactive, .graph_column_container_below, .graph_column_container_above').css('cursor', '');

            var classesToRemove =
                [ 'graph_value_element_invalid', 'graph_value_element_positive_invalid', 'graph_value_element_negative_invalid',
                    'graph_value_element_valid',   'graph_value_element_positive_valid',   'graph_value_element_negative_valid' ];

            for(var i = 0; i &lt; classesToRemove.length; i++) {
                presenter.$view.find('.' + classesToRemove[i]).removeClass(classesToRemove[i]);
            }
        }
    };

    presenter.calcScore = function () {
        var score = 0;
        presenter.$view.find('.graph_value_container').each(function(index, element) {
            if(presenter.configuration.answers[index] == parseFloat($(element).attr('current-value'))) {
                score++
            }
        });

        return score;
    };

    presenter.getScore = function() {
        if (presenter.configuration.isNotActivity || !presenter.configuration.shouldCalcScore || !presenter.isStarted) {
            return 0;
        }

        return presenter.calcScore();
    };

    presenter.getMaxScore = function() {
        if (presenter.configuration.isNotActivity) return 0;
        const examples = presenter.configuration.exampleAnswers.filter(Boolean).length;

        return presenter.configuration.answers.length - examples;
    };

    presenter.getErrorCount = function() {
        if (presenter.configuration.isNotActivity || !presenter.configuration.shouldCalcScore || !presenter.isStarted) {
            return 0;
        }

        return presenter.getMaxScore() - presenter.getScore();
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.redrawValueContainers = function () {
        var valueContainers = presenter.$view.find('.graph_value_container');
        var currentValueContainer;

        for (var i = 0; i &lt; presenter.configuration.data.length; i++) {
            for (var j = 0; j &lt; presenter.configuration.data[i].length; j++) {
                currentValueContainer = $(valueContainers[i * presenter.configuration.data[i].length + j]);
                currentValueContainer.attr('current-value', presenter.configuration.data[i][j]);
                presenter.redrawGraphValue(currentValueContainer);
            }
        }
    };

    presenter.reset = function() {
        presenter.isStarted = false;
        presenter.configuration.shouldCalcScore = true;
        presenter.isShowAnswersActive = false;
        presenter.isGradualShowAnswersActive = false;
        presenter.currentData = [];
        presenter.GSAcounter = 0;

        presenter.redrawValueContainers();

        presenter.configuration.isVisible = presenter.configuration.isVisibleByDefault;
        presenter.setVisibility(presenter.configuration.isVisible);

        presenter.setWorkMode();

        presenter.removeShowAnswersClass();
        presenter.drawExampleAnswers();
    };

    presenter.removeShowAnswersClass = function () {
        presenter.$view.find(".graph_value_container").each(function (index, element) {
            $(element).parent().find('.graph_column_container_interactive').removeClass("graph_column_container_show_answers");
            $(element).removeClass("graph_show_answers");
        });
    };


    presenter.getState = function() {
        var r = [];
        presenter.$view.find('.graph_value_container').each(function(index, element) {
            r.push(parseFloat($(element).attr('current-value')));
        });
        var state = {
            'r' : r,
            'isVisible' : presenter.configuration.isVisible,
            shouldCalcScore: presenter.configuration.shouldCalcScore,
            isStarted: presenter.isStarted
        };
        return JSON.stringify(state);
    };


    presenter.setState = function(stateString) {
        var state = JSON.parse(stateString),
            valueContainers = presenter.$view.find('.graph_value_container'),
            currentValueContainer,
            r = state.r, i,
            shouldCalcScore = state.shouldCalcScore;

        for (i = 0; i &lt; r.length; i++) {
            currentValueContainer = $(valueContainers[i]);
            currentValueContainer.attr('current-value', parseFloat(r[i]));
            presenter.redrawGraphValue(currentValueContainer);
        }

        presenter.setVisibility(state.isVisible);
        presenter.configuration.isVisible = state.isVisible;
        presenter.configuration.shouldCalcScore = shouldCalcScore;
        presenter.isStarted = state.isStarted;
        presenter.drawExampleAnswers();
    };

    presenter.setVisibility = function(isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");
    };

    presenter.hide = function() {
        presenter.setVisibility(false);
        presenter.configuration.isVisible = false;
        presenter.configuration.shouldCalcScore = true;
    };

    presenter.show = function() {
        presenter.setVisibility(true);
        presenter.configuration.isVisible = true;
        presenter.configuration.shouldCalcScore = true;
    };

    presenter.getValue = function (index) {
        presenter.configuration.shouldCalcScore = true;

        var maxIndex = presenter.$view.find('.graph_column_container').length;

        if (!ModelValidationUtils.validateIntegerInRange(index, maxIndex, 1).isValid) return;

        var $column = presenter.$view.find('.graph_column_container:eq(' + (index - 1) + ') .graph_value_container');

        return parseFloat($column.attr('current-value'));
    };

    presenter.executeCommand = function(name, params) {
        var commands = {
            'show': presenter.show,
            'hide' : presenter.hide,
            'getValue': presenter.getValue,
            'isAllOK': presenter.isAllOK
        };

        return Commands.dispatch(commands, name, params, presenter);
    };

    function prepareAndSendEvent(direction, changedBarIndex, currentValue, newValue, valueContainer) {
        var wasAllValidBeforeChange = presenter.areAllBarsValid();
        var wasThisValidBeforeChange = presenter.configuration.answers[changedBarIndex] == currentValue;
        var willAllBeValidAfterChange = presenter.areAllBarsValid();
        var willThisBeValidAfterChange = presenter.configuration.answers[changedBarIndex] == newValue;

        presenter.sendOverallScoreEvent(direction,
            valueContainer.attr('value-id'),
            newValue,
            wasAllValidBeforeChange,
            wasThisValidBeforeChange,
            willAllBeValidAfterChange,
            willThisBeValidAfterChange
        );
    }

    presenter.getProperPrecision = function (number1, number2) {
        var number1Precision = 0, number2Precision = 0;

        if (('' + number1).split('.')[1]) {
            number1Precision = ('' + number1).split('.')[1].length;
        }

        if (('' + number2).split('.')[1]) {
            number2Precision = ('' + number2).split('.')[1].length;
        }

        return Math.max(number1Precision, number2Precision);
    };

    presenter.increaseGraphValue = function(eventData) {
        eventData.stopPropagation();
        const valueContainer = $(eventData.target).parent().find('.graph_value_container');
        const changedBarIndex = presenter.$view.find('.graph_series .graph_value_container').index(valueContainer);
        const isExampleAnswer = presenter.configuration.exampleAnswers[changedBarIndex];

        if (presenter.shouldStopAction() || isExampleAnswer) { return; }

        presenter.configuration.shouldCalcScore = true;
        if (presenter.configuration.mouseData.wasDragged) {
            presenter.configuration.mouseData.wasDragged = false;
            return false;
        }

        var currentValue = parseFloat(valueContainer.attr('current-value')),
            minInteractivePoint = presenter.getMinimumInteractivePoint(valueContainer.attr('value-id')),
            newValue, newValuePrecision;

        if (currentValue == presenter.configuration.axisYMinimumValue &amp;&amp; minInteractivePoint !== currentValue) {
            // Special case when current value is minimum and can not match with those calculated with interactive step
            newValue = minInteractivePoint;
            newValuePrecision = presenter.getProperPrecision(minInteractivePoint, presenter.configuration.interactiveStep);
        } else {
            newValue = currentValue + presenter.configuration.interactiveStep;
            newValuePrecision = presenter.getProperPrecision(currentValue, presenter.configuration.interactiveStep);
        }

        if(newValue &gt; presenter.configuration.axisYMaximumValue) return;

        valueContainer.attr('current-value', newValue.toFixed(newValuePrecision));
        presenter.redrawGraphValue(valueContainer);

        if (currentValue === newValue) return;

        prepareAndSendEvent("increase", changedBarIndex, currentValue, newValue, valueContainer);
    };

    presenter.decreaseGraphValue = function(eventData) {
        eventData.stopPropagation();

        if (presenter.shouldStopAction() || presenter.isExample(eventData)) { return; }

        presenter.configuration.shouldCalcScore = true;
        if (presenter.configuration.mouseData.wasDragged) {
            presenter.configuration.mouseData.wasDragged = false;
            return false;
        }

        var valueContainer;
        if($(eventData.target).parent().hasClass('graph_value_container')) {
            // Clicked in bar
            valueContainer = $(eventData.target).parent();
        } else {
            // Clicked below bar
            valueContainer = $(eventData.target).parent().find('.graph_value_container');
        }

        var changedBarIndex = presenter.$view.find('.graph_series .graph_value_container').index(valueContainer),
            currentValue = parseFloat(valueContainer.attr('current-value')),
            maxInteractivePoint = presenter.getMaximumInteractivePoint(valueContainer.attr('value-id')),
            newValue, newValuePrecision,
            isExample = presenter.configuration.exampleAnswers[changedBarIndex];

        if (isExample) { return; }

        if (currentValue == presenter.configuration.axisYMaximumValue &amp;&amp; maxInteractivePoint !== currentValue) {
            // Special case when current value is maximum and can not match with those calculated with interactive step
            newValue = maxInteractivePoint;
            newValuePrecision = presenter.getProperPrecision(maxInteractivePoint, presenter.configuration.interactiveStep);
        } else {
            newValue = currentValue - presenter.configuration.interactiveStep;
            newValuePrecision = presenter.getProperPrecision(currentValue, presenter.configuration.interactiveStep);
        }

        if(newValue &lt; presenter.configuration.axisYMinimumValue) return;

        var splittedStep = presenter.configuration.axisYGridStep.toString().split('.'),
            precision;
        if(splittedStep[1]){
            precision = splittedStep[1].length;
            valueContainer.attr('current-value', newValue.toFixed(precision));
        }else{
            valueContainer.attr('current-value', newValue.toFixed(newValuePrecision));
        }

        presenter.redrawGraphValue(valueContainer);

        if (currentValue === newValue) return;

        prepareAndSendEvent("decrease", changedBarIndex, currentValue, newValue, valueContainer);
    };

    presenter.isExample = function (event) {
        return event.srcElement.offsetParent.className.includes(presenter.CSS_CLASSES.SHOW_EXAMPLE)
    };

    function getValueElement() {
        var $element;

        if (presenter.configuration.mouseData.$element.hasClass('graph_value_element')) {
            $element = presenter.configuration.mouseData.$element;
        } else {
            $element = presenter.configuration.mouseData.$element.parent().find('.graph_value_element');
        }

        return $element;
    }

    function isAboveXAxis($element) {
        return $element.hasClass('graph_value_element_positive');
    }

    function getColumnContainer($valueElement) {
        var containerClassName = '.graph_column_container_' + (isAboveXAxis($valueElement) ? 'above' : 'below');
        return $valueElement.parent().parent().find(containerClassName);
    }

    function isMoreThanOneFingerGesture(event) {
        return (event.touches.length || event.changedTouches.length) &gt; 1;
    }

    function mouseDownCallback (eventData) {
        if (presenter.shouldStopAction() || presenter.isExample(eventData)) { return; }

        presenter.configuration.mouseData.isMouseDown = true;
        presenter.configuration.mouseData.wasMouseDown = true;
        presenter.configuration.mouseData.wasDragged = false;
        presenter.configuration.mouseData.oldPosition.y = eventData.pageY;
        presenter.configuration.mouseData.$element = $(eventData.target);

        var $container = getValueElement().parent();

        presenter.configuration.mouseData.currentValue = parseFloat($container.attr('current-value'));
    }

    function touchStartCallback (event) {
        if (isMoreThanOneFingerGesture(event)) {
            presenter.configuration.mouseData.wasDragged = true;
            return;
        }

        event.preventDefault();

        var touch = event.touches[0] || event.changedTouches[0];
        mouseDownCallback(touch);
    }

    function columnContainerMouseDownCallback (eventData) {
        if (presenter.shouldStopAction() || presenter.isExample(eventData)) { return; }

        presenter.configuration.shouldCalcScore = true;
        presenter.configuration.mouseData.$element = $(eventData.target);
        var $element = getValueElement(), currentValue = parseFloat($element.parent().attr('current-value'));

        if (currentValue === 0) {
            presenter.configuration.mouseData.wasMouseDown = true;
            presenter.configuration.mouseData.isMouseDown = true;
            presenter.configuration.mouseData.wasDragged = false;
            presenter.configuration.mouseData.oldPosition.y = eventData.pageY;
            presenter.configuration.mouseData.$element = $element;
            presenter.configuration.mouseData.currentValue = currentValue;
        }
    }

    function columnContainerTouchStartCallback (event) {
        if (isMoreThanOneFingerGesture(event)) {
            presenter.configuration.mouseData.wasDragged = true;
            return;
        }

        event.preventDefault();

        var touch = event.touches[0] || event.changedTouches[0];

        presenter.configuration.mouseData.isColumnContainerTouchTriggered = true;
        presenter.configuration.mouseData.isColumnContainerTriggerIncrease = $(touch.target).hasClass('graph_column_container_above');
        presenter.configuration.mouseData.columnContainerEventData = touch;

        columnContainerMouseDownCallback(touch);
    }

    presenter.whichPoint = function (position, maximumValue, containerHeight, step) {
        var snapPointsCount = Math.floor(maximumValue / step), halfOfValueHigh = (containerHeight / (maximumValue / step)) / 2;

        if (position &lt; halfOfValueHigh) {
            return 0;
        } else if (position &gt;= containerHeight - halfOfValueHigh) {
            return maximumValue;
        } else {
            for (var i = 1; i &lt;= snapPointsCount; i++) {
                var snapPoint = 2 * halfOfValueHigh * i;
                if (position &gt;= snapPoint - halfOfValueHigh &amp;&amp; position &lt; snapPoint + halfOfValueHigh) return i * step;
            }
        }
    };

    presenter.getInitialData = function (valueID) {
        var series = parseInt(valueID.split(' ')[0], 10),
            index = parseInt(valueID.split(' ')[1], 10);

        return parseFloat(presenter.configuration.data[series][index]);
    };

    presenter.getMaximumInteractivePoint = function (valueID) {
        var initialData = presenter.getInitialData(valueID),
            interactiveStep = presenter.configuration.interactiveStep,
            maxYValue = presenter.configuration.axisYMaximumValue,
            maxPoint = initialData;

        while (maxPoint + interactiveStep &lt;= maxYValue) {
            maxPoint += interactiveStep;
        }

        return maxPoint
    };

    presenter.getMinimumInteractivePoint = function (valueID) {
        var initialData = presenter.getInitialData(valueID),
            interactiveStep = presenter.configuration.interactiveStep,
            minYValue = presenter.configuration.axisYMinimumValue,
            minPoint = initialData;

        while (minPoint - interactiveStep &gt;= minYValue) {
            minPoint -= interactiveStep;
        }

        return minPoint
    };

    function triggerColumnContainerClickHandler() {
        if (presenter.configuration.mouseData.isColumnContainerTriggerIncrease) {
            presenter.increaseGraphValue(presenter.configuration.mouseData.columnContainerEventData);
        } else {
            presenter.decreaseGraphValue(presenter.configuration.mouseData.columnContainerEventData);
        }
    }

    function mouseUpCallback (eventData) {
        if (presenter.shouldStopAction() || presenter.isExample(eventData)) { return; }

        presenter.isStarted = true;
        presenter.configuration.shouldCalcScore = true;

        if (!presenter.configuration.mouseData.isMouseDown) {
            if (presenter.configuration.mouseData.isColumnContainerTouchTriggered) {
                triggerColumnContainerClickHandler();
            }
            return;
        }

        presenter.configuration.mouseData.isMouseDown = false;
        if (!presenter.configuration.mouseData.wasDragged) {
            if (presenter.configuration.mouseData.isColumnContainerTouchTriggered) {
                triggerColumnContainerClickHandler();
            }
            return;
        }

        var $element = getValueElement();
        var $container = $element.parent(), height = $container.height();

        var valueContainer = presenter.configuration.mouseData.$element.parent(),
            columnContainer = getColumnContainer($element),
            columnContainerHeight = columnContainer.height(),
            newValue;

        var halfOfValueHigh;
        if (isAboveXAxis($element)) {
            halfOfValueHigh = (columnContainerHeight / presenter.configuration.axisYMaximumValue) / 2;
        } else {
            halfOfValueHigh = (columnContainerHeight / Math.abs(presenter.configuration.axisYMinimumValue)) / 2;
        }

        if (height &lt; halfOfValueHigh) {
            newValue = 0;
        } else if (height &gt;= columnContainerHeight - halfOfValueHigh) {
            newValue = isAboveXAxis($element) ? presenter.configuration.axisYMaximumValue : presenter.configuration.axisYMinimumValue;
        } else {
            var maxValue = isAboveXAxis($element) ? presenter.configuration.axisYMaximumValue : Math.abs(presenter.configuration.axisYMinimumValue);
            newValue = presenter.whichPoint(height, maxValue, columnContainerHeight, presenter.configuration.interactiveStep);
            if (!isAboveXAxis($element)) {
                newValue = -1 * newValue;
            }

        }

        var splittedStep = presenter.configuration.axisYGridStep.toString().split('.');

        if(splittedStep[1]){
            var precision = splittedStep[1].length;
            $container.attr('current-value', newValue.toFixed(precision));
        }else{
            $container.attr('current-value', newValue);
        }

        presenter.redrawGraphValue(valueContainer);

        if (presenter.configuration.mouseData.currentValue !== newValue) {
            var direction = presenter.configuration.mouseData.currentValue &lt; newValue ? "increase" : "decrease";
            var changedBarIndex = presenter.$view.find('.graph_series .graph_value_container').index(valueContainer);
            prepareAndSendEvent(direction, changedBarIndex, presenter.configuration.mouseData.currentValue, newValue, $container);
        }

        presenter.configuration.mouseData.isMouseDown = false;
        presenter.configuration.mouseData.oldPosition = { y:0 };
    }

    function touchEndCallback (event) {
        if (isMoreThanOneFingerGesture(event)) {
            presenter.configuration.mouseData.wasDragged = true;
            return;
        }

        event.preventDefault();

        mouseUpCallback();
    }

    function mouseMoveCallback (eventData) {
        if (presenter.configuration.mouseData.isMouseDown !== true || presenter.shouldStopAction() ||
            presenter.isExample(eventData)) {
            return;
        }

        presenter.configuration.shouldCalcScore = true;
        presenter.configuration.mouseData.wasDragged = true;

        var $element = getValueElement();
        var $container = $element.parent();
        var height = $container.height();
        var columnContainerHeight = getColumnContainer($element).height();
        var distance = presenter.configuration.mouseData.oldPosition.y - eventData.pageY;

        if (!isAboveXAxis($element)) {
            distance = -1 * distance;
        }

        presenter.configuration.mouseData.oldPosition.y = eventData.pageY;

        $container.css('height', (height + distance) + 'px');
        if (height + distance &gt; columnContainerHeight) {
            $container.css('height', columnContainerHeight + 'px');
        }

        if ($container.height() &lt; 1 &amp;&amp; minimumValueGraph &lt; 0) {
            if (isAboveXAxis($element)) {
                $container.css({
                    top: (presenter.chartInner.height() - presenter.drawingXPosition + Math.round(presenter.axisXLine.height() / 2)) + 'px',
                    bottom: ''
                });
                $element.removeClass('graph_value_element_positive').addClass('graph_value_element_negative');
            } else {
                $container.css({
                    bottom: (presenter.drawingXPosition - Math.round(presenter.axisXLine.height() / 2)) + 'px',
                    top: ''
                });
                $element.removeClass('graph_value_element_negative').addClass('graph_value_element_positive');
            }
        }
    }

    function touchMoveCallback (event) {
        if (isMoreThanOneFingerGesture(event)) {
            presenter.configuration.mouseData.wasDragged = true;
            return;
        }

        event.preventDefault();

        var touch = event.touches[0] || event.changedTouches[0];
        mouseMoveCallback(touch);
    }

    presenter.upgradeModel = function (model) {
        const upgradedModel = presenter.upgradeAxisYValues(model);
        return presenter.upgradeOfExampleAnswers(upgradedModel);
    };

    presenter.upgradeAxisYValues = function (model) {
        var upgradedModel = {};
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        if(model["Y axis values"] == undefined) {
            upgradedModel["Y axis values"] = "";
        }

        return upgradedModel;
    };

    presenter.upgradeOfExampleAnswers = function (model) {
        let upgradedModel = {};
        const modelAnswers = model['Answers'];
        jQuery.extend(true, upgradedModel, model); // Deep copy of model object

        modelAnswers.forEach((modelAnswer, index) =&gt; {
            if (!modelAnswer.hasOwnProperty('Example')) {
                upgradedModel['Answers'][index]['Example'] = "False";
            }
        });

        return upgradedModel;
    };


    presenter.run = function(view, model) {
        const events = ['ShowAnswers', 'GradualShowAnswers', 'HideAnswers', 'GradualHideAnswers'];
        presenter.initialize(view, model, false);

        events.forEach(event =&gt; presenter.eventBus.addEventListener(event, this));
    };

    presenter.getActivitiesCount = function () {
        const examples = presenter.configuration.exampleAnswers.filter(Boolean).length;

        return presenter.$view.find(".graph_value_container").length - examples;
    };

    presenter.createPreview = function(view, model) {
        presenter.initialize(view, model, true);
    };

    presenter.validateAxisYMaximumValue = function (model, isDecimalSeparatorSet, decimalSeparator) {
        // Y-axis maximum value
        var modelYAxisMaximumValue = model['Y axis maximum value'];

        if (isDecimalSeparatorSet) {
            modelYAxisMaximumValue = modelYAxisMaximumValue.replace(decimalSeparator, '.');
        }
        var axisYMaximumValue = ModelValidationUtils.validateFloat(modelYAxisMaximumValue);
        if (!axisYMaximumValue.isValid) {
            return { isValid: false, errorCode: 'AXIS_Y_MAXIMUM_VALUE_NOT_NUMERIC' };
        }

        return {isValid: true, value: axisYMaximumValue.parsedValue};
    };

    presenter.validateAxisYMinimumValue = function (model, isDecimalSeparatorSet, decimalSeparator) {
        //Y-axis minimum value
        var modelYAxisMinimumValue = model['Y axis minimum value'];
        minimumValueGraph = model['Y axis minimum value'];

        if (isDecimalSeparatorSet) {
            modelYAxisMinimumValue = modelYAxisMinimumValue.replace(decimalSeparator, '.');
        }
        var axisYMinimumValue = ModelValidationUtils.validateFloat(modelYAxisMinimumValue);
        if (!axisYMinimumValue.isValid) {
            return { isValid: false, errorCode: 'AXIS_Y_MINIMUM_VALUE_NOT_NUMERIC' };
        }

        return {isValid: true, value: axisYMinimumValue.parsedValue};
    };

    presenter.validateAxisYRange = function (axisYMaximumValue, axisYMinimumValue) {
        if((axisYMaximumValue &gt; 0 &amp;&amp; axisYMinimumValue &gt; 0) ||
            (axisYMaximumValue &lt; 0 &amp;&amp; axisYMinimumValue &lt; 0)) {

            return { isValid: false, errorCode: 'AXIS_Y_DOES_NOT_INCLUDE_ZERO' };
        }

        return {isValid: true};
    };

    presenter.validateAxisYGridStep = function (model, isDecimalSeparatorSet, decimalSeparator) {
         var modelYAxisGridStep = model['Y axis grid step'];
        if (isDecimalSeparatorSet) {
            modelYAxisGridStep = modelYAxisGridStep.replace(decimalSeparator, '.');
        }

        var axisYGridStep = ModelValidationUtils.validateFloat(modelYAxisGridStep);
        if(!axisYGridStep.isValid) {
            return { isValid: false, errorCode: 'AXIS_Y_GRID_STEP_NOT_NUMERIC' };
        }

        return { isValid: true, value: axisYGridStep.parsedValue};

    };

    presenter.validateInteractiveStep = function (model, isDecimalSeparatorSet, decimalSeparator) {
        var isInteractive = ModelValidationUtils.validateBoolean(model['Interactive']);
        var interactiveStep;

        if(isInteractive) {
            var modelInteractiveStep = model['Interactive step'];
            if (isDecimalSeparatorSet) {
                modelInteractiveStep = modelInteractiveStep.replace(decimalSeparator, '.');
            }

            interactiveStep = ModelValidationUtils.validateFloat(modelInteractiveStep);
            if(!interactiveStep.isValid) {
                return { isValid: false, errorCode: 'INTERACTIVE_STEP_NOT_NUMERIC' };
            }

            if(interactiveStep.parsedValue &lt;= 0) {
                return { isValid: false, errorCode: 'INTERACTIVE_STEP_NOT_POSITIVE' };
            }

            interactiveStep = interactiveStep.parsedValue;
        }

        return {isValid: true, interactiveStep: interactiveStep, isInteractive: isInteractive};
    };


    presenter.validateModel = function (model) {
        var decimalSeparator = model["Decimal separator"];
        var isDecimalSeparatorSet = !ModelValidationUtils.isStringEmpty(decimalSeparator);

        var validatedAxisYMaximumValue = presenter.validateAxisYMaximumValue(model, isDecimalSeparatorSet, decimalSeparator);
        if(!validatedAxisYMaximumValue.isValid) {
            return validatedAxisYMaximumValue;
        }

        var validatedAxisYMinimumValue = presenter.validateAxisYMinimumValue(model, isDecimalSeparatorSet, decimalSeparator);
        if (!validatedAxisYMinimumValue.isValid) {
            return validatedAxisYMinimumValue;
        }

        var validatedAxisYRange = presenter.validateAxisYRange(validatedAxisYMaximumValue.value, validatedAxisYMinimumValue.value);
        if (!validatedAxisYRange.isValid) {
            return validatedAxisYRange;
        }

        // Y-axis grid step
        var validatedAxisYGridStep = presenter.validateAxisYGridStep(model, isDecimalSeparatorSet, decimalSeparator);
        if (!validatedAxisYGridStep.isValid) {
            return validatedAxisYGridStep;
        }

        // Interactive (step) mode
        var validatedInteractiveStep = presenter.validateInteractiveStep(model, isDecimalSeparatorSet,
            decimalSeparator);

        if (!validatedInteractiveStep.isValid) {
            return validatedInteractiveStep;
        }
        var isInteractive = validatedInteractiveStep.isInteractive;

        var isNotActivity;
        try {
            isNotActivity = (model['isNotActivity'].toLowerCase() === 'true');
        } catch (_) {
            isNotActivity = false;
        }
        var parsedColors = presenter.parseColors(model);

        var graphConfiguration = {
            "isDecimalSeparatorSet": isDecimalSeparatorSet,
            "decimalSeparator": decimalSeparator,
            "Series colors": parsedColors,
            "axisYMaximumValue": validatedAxisYMaximumValue.value,
            "axisYMinimumValue": validatedAxisYMinimumValue.value
        };
        // Data
        var validatedData = presenter.validateData(model, graphConfiguration);

        if(!validatedData.isValid) {
            return validatedData
        }

        var validatedAxisXBarsDescriptions = presenter.validateAxisXBarsDescriptions(model, validatedData.value.barsCount);

        if(!validatedAxisXBarsDescriptions.isValid) {
            return validatedAxisXBarsDescriptions
        }

        var validatedAxisXSeriesDescriptions = presenter.validateAxisXSeriesDescriptions(model, validatedData.value.validRows);
        if (!validatedAxisXSeriesDescriptions.isValid) {
            return validatedAxisXSeriesDescriptions;
        }

        if (isInteractive) {
            var validatedAnswers = presenter.validateAnswers(model['Answers'], validatedData.value.barsCount);
            if (!validatedAnswers.isValid) {
                return validatedAnswers;
            }
            var results = presenter.parseResults(validatedData.value.parsedData, validatedAnswers.answers);

        } else {
            var validatedAnswers = {answers: []};
            var results = [];
        }

        const validatedExampleAnswers = presenter.validateExampleAnswers(model['Answers']);

        var validatedAxisYValues = presenter.validateAxisYValues(model, validatedAxisYMaximumValue.value,
            validatedAxisYMinimumValue.value, isDecimalSeparatorSet);

        if (!validatedAxisYValues.isValid) {
            return validatedAxisYValues;
        }

        return {
            isValid: true,
            ID: model.ID,
            isVisible: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            isNotActivity: isNotActivity,
            shouldCalcScore: false,
            decimalSeparator: decimalSeparator,
            isDecimalSeparatorSet: isDecimalSeparatorSet,
            axisYMaximumValue: validatedAxisYMaximumValue.value,
            axisYMinimumValue: validatedAxisYMinimumValue.value,
            axisYGridStep: validatedAxisYGridStep.value,
            data: validatedData.value.parsedData,
            isInteractive: isInteractive,
            interactiveStep: validatedInteractiveStep.interactiveStep,
            mouseData: {
                isMouseDown : false,
                oldPosition : { y : 0 },
                isMouseDragged : false
            },
            showXAxisBarsDescriptions: validatedAxisXBarsDescriptions.value.showXAxisBarsDescriptions,
            axisXBarsDescriptions: validatedAxisXBarsDescriptions.value.axisXBarsDescriptions,
            showXAxisSeriesDescriptions: validatedAxisXSeriesDescriptions.value.showXAxisSeriesDescriptions,
            axisXSeriesDescriptions: validatedAxisXSeriesDescriptions.value.axisXSeriesDescriptions,
            seriesColors: parsedColors,
            barsCount: validatedData.value.barsCount,
            columnsCount: validatedData.value.columnsCount,
            validRows: validatedData.value.validRows,
            results: results,
            answers: validatedAnswers.answers,
            exampleAnswers: validatedExampleAnswers,
            axisYValues: {fixedValues: validatedAxisYValues.fixedValues, cyclicValues: validatedAxisYValues.cyclicValues}
        };
    };

    presenter.parseResults = function (data, answers) {
        var results = [];

        var k = 0;
        var i, j;

        for (i=0; i &lt; data.length; i++) {
            var a = [];
            for (j = 0; j &lt; data[i].length; j++) {
                a.push(parseInt(answers[k++]) ===  parseInt(data[i][j]));
            }

            results.push(a);
        }

        return results;
    };

    presenter.parseAxisXBarsDescriptions = function (model, showXAxisBarsDescriptions) {
        var i;
        var xAxisBarsDescriptions = [];

        if (showXAxisBarsDescriptions &amp;&amp; typeof(model['X axis bars descriptions']) != 'undefined') {
            for (i = 0; i &lt; model['X axis bars descriptions'].length; i++) {
                xAxisBarsDescriptions.push(model['X axis bars descriptions'][i]['Description']);
            }
        }

        return xAxisBarsDescriptions;
    };

    presenter.parseAxisXSeriesDescriptions = function (model,showXAxisSeriesDescriptions) {
        var xAxisSeriesDescriptions = [];
        var i;
        if (showXAxisSeriesDescriptions &amp;&amp; typeof(model['X axis series descriptions']) != 'undefined') {
            for (i = 0; i &lt; model['X axis series descriptions'].length; i++) {
                xAxisSeriesDescriptions.push(model['X axis series descriptions'][i]['Description']);
            }
        }

        return xAxisSeriesDescriptions;
    };

    presenter.validateData = function(model, graphConfiguration) {

        var parsedData = presenter.parseData(model, graphConfiguration.isDecimalSeparatorSet, graphConfiguration.decimalSeparator);

        // Read data
        var currentValue;
        var maximumValue = null;
        var minimumValue = null;
        var row, column;
        var validRows = 0;
        var columnsCount = null;
        var barsCount = 0;

        // Validate data and find maximum value
        for (row = 0; row &lt; parsedData.length; row++) {
            // Ensure that rows have valid syntax
            if (parsedData[row] === null) {
                return {isValid: false, errorCode: "DATA_ROW_MALFORMED", errorMessageSubstitutions: { row: row + 1 }};
            }

            // Skip empty rows
            if (parsedData[row].length === 0) {
                continue;
            }
            validRows++;
            // Ensure that rows have valid amount of columns
            if (parsedData[row].length &lt; 1) {
                return {isValid: false, errorCode: "DATA_ROW_NOT_ENOUGH_COLUMNS", errorMessageSubstitutions: { row: row + 1 }};
            }

            if (columnsCount === null) {
                columnsCount = parsedData[row].length;
            } else if (columnsCount != parsedData[row].length) {
                return {isValid: false, errorCode: "DATA_ROW_DIFFERENT_COLUMNS_COUNT", errorMessageSubstitutions:  { row: row + 1 }};
            }

            // Save min/max value and ensure that data is numeric
            for (column = 0; column &lt; parsedData[row].length; column++) {
                currentValue = parseFloat(parsedData[row][column]);

                if (isNaN(currentValue)) {
                    return {isValid: false, errorCode: "DATA_ROW_VALUE_NOT_NUMERIC", errorMessageSubstitutions: { row: row + 1, column: column, value: parsedData[row][column] }};
                }

                if (maximumValue === null || currentValue &gt; maximumValue) {
                    maximumValue = currentValue;
                }

                if (minimumValue === null || currentValue &lt; minimumValue) {
                    minimumValue = currentValue;
                }

                parsedData[row][column] = currentValue;
            }
            // Count amount of bars
            barsCount += parsedData[row].length;
        }

        if (graphConfiguration.axisYMaximumValue &lt; maximumValue) {
            return {isValid: false, errorCode: 'AXIS_Y_MAXIMUM_VALUE_TOO_SMALL', errorMessageSubstitutions: { value: maximumValue, range: graphConfiguration.axisYMaximumValue }};
        }

        if (graphConfiguration.axisYMinimumValue &gt; minimumValue) {
            return {isValid: false, errorCode: "AXIS_Y_MINIMUM_VALUE_TOO_BIG", errorMessageSubstitutions:  { value: minimumValue, range: graphConfiguration.axisYMinimumValue}};
        }

        if (graphConfiguration["Series colors"].length != columnsCount) {
            return {isValid: false, errorCode: "SERIES_COLORS_AMOUNT_INVALID"}
        }

        return {
            isValid: true,
            value: {
                maximumValue: maximumValue,
                minimumValue: minimumValue,
                validRows: validRows,
                barsCount: barsCount,
                columnsCount: columnsCount,
                parsedData: parsedData
            }
        };
    };

    presenter.parseColors = function(model) {
        var colors = [];

        var i;
        for (i = 0; i &lt; model['Series colors'].length; i++) {
            colors.push(model['Series colors'][i]['Color']);
        }

        return colors;
    };

    presenter.parseData = function(model, isDecimalSeparatorSet, decimalSeparator) {
        var data = $.csv2Array(model['Data']);

        try {
            if (isDecimalSeparatorSet) {
                for (var i= 0; i &lt; data.length; i++) {
                    for (var j= 0; j &lt; data[i].length; j++) {
                        data[i][j] = data[i][j].replace(decimalSeparator, '.');
                    }
                }
            }
        } catch ( _ ) {
            //when user inputs invalid csv syntax in data property, data is null
            //it creates error which shows popup in lesson &amp; editor
            //this error is validated in validateData
            return data
        }

        return data;
    };

    presenter.validateAxisXBarsDescriptions = function(model, barsCount) {
        var showXAxisBarsDescriptions = typeof(model['Show X axis bars descriptions']) != 'undefined' &amp;&amp;
            model['Show X axis bars descriptions'] === 'True';

        var parsedXAxisBarsDescriptions = presenter.parseAxisXBarsDescriptions(model, showXAxisBarsDescriptions);

        if (showXAxisBarsDescriptions &amp;&amp; parsedXAxisBarsDescriptions.length != barsCount) {
            return {isValid: false, errorCode: "AXIS_X_BARS_DESCRIPTIONS_AMOUNT_INVALID",
                errorMessageSubstitutions:{ bars: barsCount, descriptions: parsedXAxisBarsDescriptions.length }};
        }

        return {
            isValid: true,
            value: {
                showXAxisBarsDescriptions: showXAxisBarsDescriptions,
                axisXBarsDescriptions: parsedXAxisBarsDescriptions
            }
        };
    };

    presenter.validateAxisXSeriesDescriptions = function(model, validRows) {
        var showXAxisSeriesDescriptions = typeof(model['Show X axis series descriptions']) != 'undefined' &amp;&amp;
            model['Show X axis series descriptions'] === 'True';
        var parsedXAxisSeriesDescriptions = presenter.parseAxisXSeriesDescriptions(model, showXAxisSeriesDescriptions);

        if (showXAxisSeriesDescriptions &amp;&amp; parsedXAxisSeriesDescriptions.length != validRows) {
            return {isValid: false, errorCode: "AXIS_X_SERIES_DESCRIPTIONS_AMOUNT_INVALID",
                errorMessageSubstitutions: { series: validRows, descriptions: parsedXAxisSeriesDescriptions.length }};
        }

        return {
            isValid: true,
            value: {
                showXAxisSeriesDescriptions: showXAxisSeriesDescriptions,
                axisXSeriesDescriptions: parsedXAxisSeriesDescriptions
            }
        }
    };

    presenter.isFloat = function (value) {
        value = value.trim();
        if (ModelValidationUtils.isStringEmpty(value)) {
            return false;
        }

        if (value.charAt(0) == "+") {
            return false;
        }

        var too_many_zeroes = /^0{2,}/;
        if (too_many_zeroes.test(value)) {
            return false;
        }

        if ( value == "-0") {
            return false;
        }

        if (value.charAt(0) == "-") {
            value = value.slice(1, value.length);
        }

        var i, commas_number = 0;
        var digits = /[0-9]/;

        for(i = 0; i &lt; value.length; i++) {
            if (value.charAt(i) == ".") {
                if (commas_number == 1) {
                    return false;
                }
                commas_number ++;
            } else {
                if (digits.test(value.charAt(i))) {
                    continue;
                } else {
                    return false;
                }
            }
        }

        return true;
    };

    presenter.checkIfValueInAxisRange = function (value, yMax, yMin) {
        if (value &gt; yMax) {
            return {isValid: false, errorCode: "YAV_03"};
        }

        if (value &lt; yMin) {
            return {isValid: false, errorCode: "YAV_04"};
        }

        return {isValid: true, value: value};
    };

    presenter.checkCyclicValue = function (value, yMax, yMin) {
        if (!presenter.isFloat(value)) {
            return {isValid: false, errorCode: "YAV_01"};
        }

        value = parseFloat(value);

        if (value == 0) {
            return {isValid: false, errorCode: "YAV_02"};
        }

        if (value &lt; 0) {
            return {isValid: false, errorCode: "YAV_05"};
        }


        var validatedValue = presenter.checkIfValueInAxisRange(value, yMax, yMin);

        return validatedValue;
    };

    presenter.checkFixedValue = function (value, yMax, yMin) {
        if (!presenter.isFloat(value)) {
            return {isValid: false, errorCode: "YAV_01"};
        } else {
            var validatedValue = presenter.checkIfValueInAxisRange(parseFloat(value), yMax, yMin);
        }

        return validatedValue;
    };

    presenter.createAxisYValues = function (fixedValues, cyclicValues, yMax, yMin) {
        var values = [];
        var i;

        if (fixedValues == undefined &amp;&amp; cyclicValues == undefined) {
            cyclicValues = [presenter.configuration.axisYGridStep];
        }

        if (fixedValues != undefined) {
            for(i = 0; i &lt; fixedValues.length; i++) {
                values.push(fixedValues[i]);
            }
        }

        if (cyclicValues != undefined) {

            for(i = 0; i &lt; cyclicValues.length; i++) {
                var step = cyclicValues[i];

                var splittedStep = step.toString().split('.');

                var value;
                if(splittedStep[1]){
                    var commaLength = splittedStep[1].length;
                    for(value = step; value.toFixed(commaLength) &lt;= yMax; value += step) {
                        values.push(value.toFixed(commaLength));
                    }
                }else{
                    for(value = step; value &lt;= yMax; value += step) {
                        values.push(value);
                    }
                }

                for(value = -step; value &gt;= yMin; value -= step) {
                    values.push(value);
                }
            }
        }

        return values;
    };

    presenter.validateAxisYValues = function(model, yMax, yMin, isDecimalSeparatorSet) {
        var values = model["Y axis values"];
        var i;

        var parsedAxisYValues = {isValid: true, fixedValues: undefined, cyclicValues: undefined};

        if (ModelValidationUtils.isStringEmpty(values.trim())) {
            return parsedAxisYValues;
        }

        values = values.split(";");

        if(isDecimalSeparatorSet) {
            for(i = 0; i &lt; values.length; i++) {
                values[i] = values[i].replace(model["Decimal separator"], '.');
            }
        }

        for(i = 0; i &lt; values.length; i++) {
            var value = values[i].trim();
            var endChar = value.length - 1;
            var validatedValue;

            if(value.charAt(endChar) == "*") {
                validatedValue = presenter.checkCyclicValue(value.slice(0, endChar), yMax, yMin);

                if (!validatedValue.isValid) {
                    return validatedValue;
                }

                if (parsedAxisYValues.cyclicValues == undefined) {
                    parsedAxisYValues.cyclicValues = [];
                }

                if (parsedAxisYValues.cyclicValues.indexOf(validatedValue.value) == -1) {
                    parsedAxisYValues.cyclicValues.push(validatedValue.value);
                    continue;
                }

                return {isValid: false, errorCode: "YAV_06"};

            } else {
                validatedValue = presenter.checkFixedValue(value, yMax, yMin);

                if (!validatedValue.isValid) {
                    return validatedValue;
                }

                if (parsedAxisYValues.fixedValues == undefined) {
                    parsedAxisYValues.fixedValues = [];
                }

                if (parsedAxisYValues.fixedValues.indexOf(validatedValue.value) == -1) {
                    parsedAxisYValues.fixedValues.push(validatedValue.value);
                    continue;
                }

                return {isValid: false, errorCode: "YAV_06"};
            }
        }

        return parsedAxisYValues;
    };

    presenter.validateAnswers = function (answers, barsCount) {
        var validatedAnswers = [], i;

        for (i = 0; i &lt; answers.length; i++) {
            var answer = answers[i]['Answer'];

            if (presenter.configuration.isDecimalSeparatorSet) {
                answer = answer.replace(presenter.configuration.decimalSeparator, '.');
            }

            var parseAnswer = parseFloat(answer);
            if (isNaN(parseAnswer)) {
                return { isValid: false, errorCode: 'ANSWER_NOT_NUMERIC', errorMessageSubstitutions: { answer: i + 1 } };
            }

            validatedAnswers.push(parseAnswer);
        }

        if (validatedAnswers.length != barsCount) {
            return { isValid: false, errorCode: 'ANSWERS_AMOUNT_INVALID', errorMessageSubstitutions: { answers: validatedAnswers.length, bars: barsCount } };
        }

        return {
            isValid: true,
            answers: validatedAnswers
        };
    };

    presenter.validateExampleAnswers = function (answers) {
        const exampleAnswers = [];
        if (!answers) { return exampleAnswers; }
        answers.forEach(answer =&gt; {
            exampleAnswers.push(ModelValidationUtils.validateBoolean(answer['Example']));
        });

        return exampleAnswers;
    }

    presenter.drawGrid = function (grid) {
        var axisYGridStep = presenter.configuration.axisYGridStep;
        var drawingGridStep = presenter.chartInner.height() * axisYGridStep / presenter.absoluteRange;
        var i;


        for (i = axisYGridStep; i &lt;= presenter.configuration.axisYMaximumValue; i += axisYGridStep) {
            var currentGridBlock = $('&lt;div class="graph_grid_block graph_grid_block_above"&gt;&lt;/div&gt;');
            grid.append(currentGridBlock);
            currentGridBlock.css({
                height: (drawingGridStep - parseInt(currentGridBlock.css('borderTopWidth'))) + 'px',
                bottom: presenter.drawingXPosition - drawingGridStep + (drawingGridStep * i / axisYGridStep)
            });
        }


        for (i = -1 * axisYGridStep; i &gt;= presenter.configuration.axisYMinimumValue; i -= axisYGridStep) {
            currentGridBlock = $('&lt;div class="graph_grid_block graph_grid_block_below"&gt;&lt;/div&gt;');
            grid.append(currentGridBlock);
            currentGridBlock.css({
                height: (drawingGridStep - parseInt(currentGridBlock.css('borderBottomWidth'))) + 'px',
                bottom: presenter.drawingXPosition + (drawingGridStep * i / axisYGridStep)
            });
        }

        return grid;
    };

    presenter.createGridDescriptions = function (innerContainer) {
        var gridDescription, gridDescriptionText;
        var maximumGridDescriptionWidth = null;
        var currentGridDescriptionWidth;
        var i;

        var fixedValues = presenter.configuration.axisYValues.fixedValues;
        var cyclicValues = presenter.configuration.axisYValues.cyclicValues;
        var yMax = presenter.configuration.axisYMaximumValue;
        var yMin = presenter.configuration.axisYMinimumValue;
        var values = presenter.createAxisYValues(fixedValues, cyclicValues, yMax, yMin);


        for (i = 0; i &lt; values.length; i++) {
            gridDescription = $('&lt;div class="graph_grid_description"&gt;&lt;/div&gt;');
            gridDescription.addClass('graph_grid_description_' + ("" + values[i]).toString().replace('.', '_'));
            gridDescriptionText = "" + values[i];
            if (presenter.configuration.isDecimalSeparatorSet) {
                gridDescriptionText = gridDescriptionText.replace('.', presenter.configuration.decimalSeparator);
            }
            gridDescription.text(gridDescriptionText);
            innerContainer.append(gridDescription);

            currentGridDescriptionWidth = gridDescription.width();
            if (maximumGridDescriptionWidth === null || currentGridDescriptionWidth &gt; maximumGridDescriptionWidth) {
                maximumGridDescriptionWidth = currentGridDescriptionWidth;
            }
        }

        presenter.$view.find('.graph_grid_description').css('width', maximumGridDescriptionWidth + 'px');

        return { maximumGridDescriptionWidth: maximumGridDescriptionWidth , axisYValues: values};
    };

    presenter.positionAxisYValues = function (values, xAxisDescriptionMargin) {
        var i, containerHeight = presenter.chartInner.height();

        for (i = 0; i &lt; values.length; i++) {
            //rescale every value to 0 - positive, and calculate what percentage of height they are
            var descriptionElementHeight = (values[i] - presenter.configuration.axisYMinimumValue) ;
            descriptionElementHeight = (descriptionElementHeight / presenter.absoluteRange) * containerHeight;

            presenter.$view.find('.graph_grid_description_' + String(values[i]).toString().replace('.', '_')).each(function (index, element) {
                $(element).css({
                    bottom: (descriptionElementHeight - ($(element).height() / 2) + xAxisDescriptionMargin) + 'px'
                });
            });
        }
    };

    presenter.deleteCommands = function () {
        delete presenter.getMaxScore;
        delete presenter.getScore;
        delete presenter.setState;
        delete presenter.getState;
        delete presenter.getValue;
    };

    presenter.initialize = function(view, model, isPreview) {
        presenter.$view = $(view);
        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);

        presenter.configuration = validatedModel;

        if (!validatedModel.isValid) {
            presenter.showErrorMessage(presenter.ERROR_MESSAGES[validatedModel.errorCode], validatedModel.errorMessageSubstitutions);
            presenter.deleteCommands();
            return;
        }

        presenter.setVisibility(presenter.configuration.isVisible || isPreview);

        if (isPreview) presenter.configuration.isInteractive = false;

        presenter.drawGraph(view, model);
        presenter.drawExampleAnswers();
    };

    presenter.drawGraph = function (view, model) {
        // Read data
        var i, j;
        var validRows = presenter.configuration.validRows;
        var columnsCount = presenter.configuration.columnsCount;

        var showXAxisBarsDescriptions = presenter.configuration.showXAxisBarsDescriptions;
        var showXAxisSeriesDescriptions = presenter.configuration.showXAxisSeriesDescriptions;

        var xAxisBarsDescriptions = presenter.configuration.axisXBarsDescriptions;
        var xAxisSeriesDescriptions = presenter.configuration.axisXSeriesDescriptions;



        // Draw graph's containers
        var outerContainer = $('&lt;div class="graph_container_outer"&gt;&lt;/div&gt;');
        presenter.$view.append(outerContainer);

        var innerContainer = $('&lt;div class="graph_container_inner"&gt;&lt;/div&gt;');
        outerContainer.append(innerContainer);

        // Draw axis descriptions
        var axisYDescription = $('&lt;div class="graph_axis_description graph_axis_y_description"&gt;&lt;/div&gt;');
        axisYDescription.text(model['Y axis description']);
        innerContainer.append(axisYDescription);

        var axisXDescription = $('&lt;div class="graph_axis_description graph_axis_x_description"&gt;&lt;/div&gt;');
        axisXDescription.text(model['X axis description']);
        innerContainer.append(axisXDescription);


        // Draw outer chart container and set its position using
        // Y axis description's height and X axis description's width
        // plus 4px margin
        var chartOuter = $('&lt;div class="graph_chart_outer"&gt;&lt;/div&gt;');
        chartOuter.css({
            top: (axisYDescription.height() + 4) + 'px',
            right: (axisXDescription.width() + 4) + 'px'
        });
        innerContainer.append(chartOuter);

        // Create axis Y Values
        var gridDescriptionsObject = presenter.createGridDescriptions(innerContainer);
        var maximumGridDescriptionWidth = gridDescriptionsObject.maximumGridDescriptionWidth;
        var axisYValues = gridDescriptionsObject.axisYValues;

        // Draw inner chart container and set its position using
        // Y axis descriptions' width plus 4px margin
        var xAxisDescriptionMargin = 0;
        if (showXAxisBarsDescriptions) {
            xAxisDescriptionMargin += 20;
        }

        if (showXAxisSeriesDescriptions) {
            xAxisDescriptionMargin += 20;
        }

        presenter.chartInner = $('&lt;div class="graph_chart_inner"&gt;&lt;/div&gt;');
        presenter.chartInner.css({
            left: (maximumGridDescriptionWidth + 4) + 'px',
            bottom: (xAxisDescriptionMargin) + 'px'
        });
        chartOuter.append(presenter.chartInner);


        // Calculate position of axis X, grid &amp; interactive step
        presenter.absoluteRange = presenter.configuration.axisYMaximumValue - presenter.configuration.axisYMinimumValue;
        var absoluteXPosition = presenter.absoluteRange - presenter.configuration.axisYMaximumValue;

        presenter.drawingXPosition = presenter.chartInner.height() * absoluteXPosition / presenter.absoluteRange;

        // Move Y axis descriptions to the right place and draw grid
        var grid = $('&lt;div class="graph_grid"&gt;&lt;/div&gt;');
        presenter.chartInner.append(grid);

        grid = presenter.drawGrid(grid);
        presenter.positionAxisYValues(axisYValues, xAxisDescriptionMargin);

        // Draw axis X
        presenter.axisXLine = $('&lt;div class="graph_axis_x_line graph_axis_line"&gt;&lt;/div&gt;');
        presenter.chartInner.append(presenter.axisXLine);
        presenter.axisXLine.css('bottom', (presenter.drawingXPosition - Math.round(presenter.axisXLine.height() / 2)) + 'px');

        var axisXArrow = $('&lt;div class="graph_axis_x_arrow graph_axis_arrow"&gt;&lt;/div&gt;');
        presenter.chartInner.append(axisXArrow);


        // Prepare drawing of the graph itself
        var serieContainer;
        var serieElement;
        var columnContainer;
        var columnContainerBelow;
        var columnContainerAbove;
        var valueContainer;
        var valueElement;
        var columnDescription;
        var serieDescription;

        var serieWidth = Math.round(parseInt(presenter.chartInner.width() / validRows));

        // Adjust serieWidth to previous value that divides by columnsCount without rest
        while (serieWidth % columnsCount !== 0) {
            serieWidth--;
        }

        var columnWidth = (100.0 / columnsCount) + '%';

        var series = $('&lt;div class="graph_series"&gt;&lt;/div&gt;');
        presenter.axisXLine.before(series);


        for (i = 0; i &lt; presenter.configuration.data.length; i++) {
            // Skip empty rows
            if (presenter.configuration.data[i].length === 0) {
                continue;
            }

            serieContainer = $('&lt;div class="graph_serie_container"&gt;&lt;/div&gt;');
            serieContainer.css('width', serieWidth);

            serieElement = $('&lt;div class="graph_serie_element graph_serie_size"&gt;&lt;/div&gt;');
            serieContainer.append(serieElement);


            if (showXAxisSeriesDescriptions) {
                serieDescription = $('&lt;div class="graph_serie_description"&gt;&lt;/div&gt;');
                serieDescription.text(xAxisSeriesDescriptions[i]);
                serieElement.append(serieDescription);

                if (showXAxisBarsDescriptions) {
                    serieDescription.css('bottom', '-40px');
                } else {
                    serieDescription.css('bottom', '-20px');
                }
            }


            for (j = 0; j &lt; presenter.configuration.data[i].length; j++) {
                columnContainer = $('&lt;div class="graph_column_container"&gt;&lt;/div&gt;');
                columnContainer.css('width', columnWidth);

                if (presenter.configuration.isInteractive) {
                    columnContainerBelow = $('&lt;div class="graph_value_size graph_column_container_interactive graph_column_container_below"&gt;&lt;/div&gt;');
                    columnContainerAbove = $('&lt;div class="graph_value_size graph_column_container_interactive graph_column_container_above"&gt;&lt;/div&gt;');
                    columnContainer.append(columnContainerBelow);
                    columnContainer.append(columnContainerAbove);
                    columnContainerBelow.css('top', (presenter.chartInner.height() - presenter.drawingXPosition) + 'px');
                    columnContainerAbove.css('bottom', presenter.drawingXPosition + 'px');

                    columnContainerAbove.click(presenter.increaseGraphValue);
                    columnContainerBelow.click(presenter.decreaseGraphValue);
                }

                valueContainer = $('&lt;div class="graph_value_container"&gt;&lt;/div&gt;');

                valueElement = $('&lt;div class="graph_value_element graph_value_size"&gt;&lt;/div&gt;');
                if (presenter.configuration.isInteractive) {
                    valueElement.addClass('graph_value_element_interactive');
                    valueElement.click(presenter.decreaseGraphValue);
                }

                valueElement.css('backgroundColor', presenter.configuration.seriesColors[j]);

                valueContainer.attr('current-value', presenter.configuration.data[i][j]);
                valueContainer.attr('value-id', i + ' ' + j);
                valueContainer.append(valueElement);

                if (presenter.configuration.isInteractive) {
                    $(valueContainer).mousedown(mouseDownCallback);
                    valueContainer[0].ontouchstart = touchStartCallback;
                }

                if (showXAxisBarsDescriptions) {
                    columnDescription = $('&lt;div class="graph_column_description graph_value_size"&gt;&lt;/div&gt;');
                    columnDescription.text(xAxisBarsDescriptions[i * columnsCount + j]);
                    columnContainer.append(columnDescription);
                }

                presenter.redrawGraphValue(valueContainer);
                columnContainer.append(valueContainer);

                if (presenter.configuration.isInteractive) {
                    $(columnContainer).mousedown(columnContainerMouseDownCallback);
                    columnContainer[0].ontouchstart = columnContainerTouchStartCallback;
                    $(columnContainer).mouseup(mouseUpCallback);
                    columnContainer[0].ontouchend = touchEndCallback;
                    $(columnContainer).mousemove(mouseMoveCallback);
                    columnContainer[0].ontouchmove = touchMoveCallback;
                }

                serieElement.append(columnContainer);
            }

            series.append(serieContainer);
        }

        // Move axis X description &amp; arrow to right place
        axisXDescription.css('bottom', (presenter.drawingXPosition - Math.round(axisXDescription.height() / 2) + xAxisDescriptionMargin) + 'px');
        axisXArrow.css('bottom', (presenter.drawingXPosition - parseInt(axisXArrow.css('borderLeftWidth'))) + 'px');

        // Draw axis Y
        var axisYLine = $('&lt;div class="graph_axis_y_line graph_axis_line"&gt;&lt;/div&gt;');
        presenter.chartInner.append(axisYLine);

        var axisYArrow = $('&lt;div class="graph_axis_y_arrow graph_axis_arrow"&gt;&lt;/div&gt;');
        presenter.chartInner.append(axisYArrow);

        // Move axis Y description &amp; arrow to right place
        var axisYDescriptionLeft = parseInt(presenter.chartInner.css('left')) - Math.round(axisYDescription.width() / 2);
        if (axisYDescriptionLeft &lt; 0) {
            axisYDescriptionLeft = 0;
        }
        axisYDescription.css('left', axisYDescriptionLeft + 'px');
    };

    presenter.drawExampleAnswers = function () {
        const isAnyExampleAnswer = presenter.configuration.exampleAnswers.some(answer =&gt; answer === true);
        if (!isAnyExampleAnswer) {
            return;
        }

        presenter.configuration.exampleAnswers.forEach((answer, index) =&gt; {
            if (answer) {
                const graphAnswers = presenter.$view.find(".graph_value_container");

                presenter.addAnswerToGraph(index, $(graphAnswers[index]), true);
            }
        })
    }

    presenter.onEventReceived = function (eventName, eventData) {
        switch (eventName) {
            case 'GradualShowAnswers':
                presenter.gradualShowAnswers(eventData);
                break;

            case 'ShowAnswers':
                presenter.showAnswers();
                break;

            case 'HideAnswers':
                presenter.hideAnswers();
                break;

            case 'GradualHideAnswers':
                presenter.gradualHideAnswers();
                break;
        }
    };

    presenter.showAnswers = function () {
        if (presenter.configuration.isNotActivity) return;

        if (presenter.errorMode){
            presenter.setWorkMode();
        }

        presenter.isShowAnswersActive = true;
        presenter.currentData = [];
        presenter.setCurrentState();

        presenter.$view.find(".graph_value_container").each(function (index, element) {
            const isExample = presenter.configuration.exampleAnswers[index];
            if (!isExample) {
                presenter.addAnswerToGraph(index, element);
            }
        });
    };

    presenter.hideAnswers = function () {
        if (presenter.configuration.isNotActivity || !presenter.isShowAnswersActive) {
            return;
        }

        presenter.$view.find(".graph_value_container").each(function (index, element) {
            presenter.removeAnswerFromGraph(index, element);
        });

        presenter.isShowAnswersActive = false;
        presenter.drawExampleAnswers();
    };

    presenter.gradualShowAnswers = function (eventData) {
        if (eventData.moduleID !== presenter.configuration.ID) return;

        if (presenter.errorMode){
            presenter.setWorkMode();
        }

        let itemIndex = parseInt(eventData.item, 10);
        const graphAnswers = presenter.$view.find(".graph_value_container");
        presenter.isGradualShowAnswersActive = true;

        itemIndex = itemIndex &lt; presenter.GSAcounter ? presenter.GSAcounter : itemIndex;
        if (presenter.configuration.exampleAnswers[presenter.GSAcounter]) {
            itemIndex += 1;
        }

        presenter.setCurrentState();
        presenter.addAnswerToGraph(itemIndex, $(graphAnswers[itemIndex]));

        presenter.GSAcounter = ++itemIndex;
        presenter.isGradualShowAnswersActive = true;
    }
    
    presenter.gradualHideAnswers = function () {
        if (presenter.configuration.isNotActivity || !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.$view.find(".graph_value_container").each(function (index, element) {
            presenter.removeAnswerFromGraph(index, element);
        });

        presenter.isGradualShowAnswersActive = false;
        presenter.currentData = [];
        presenter.GSAcounter = 0;
        presenter.drawExampleAnswers();
    }

    presenter.setCurrentState = function () {
        presenter.$view.find(".graph_value_container").each(function (index, element) {
            presenter.currentData[index] = $(element).attr("current-value");
        });
    }

    presenter.shouldStopAction = function () {
        // check if the column is example or not
        return presenter.isDisplayingAnswers() || presenter.errorMode;
    }

    presenter.isDisplayingAnswers = function () {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    }

    presenter.addAnswerToGraph = function (index, element, isExample = false) {
        const currentValue = presenter.configuration.answers[index],
            valueContainer = $(element),
            $columnContainer = valueContainer.parent('').find('.graph_column_container_interactive');
        if(currentValue &gt;= 0) {
            valueContainer.css({
                bottom: (presenter.drawingXPosition - Math.round(presenter.axisXLine.height() / 2)) + 'px',
                height: parseFloat(currentValue / presenter.absoluteRange) * 100 + '%',
                top: ''
            });
        } else {
            valueContainer.css({
                height: parseFloat(currentValue * -1 / presenter.absoluteRange) * 100 + '%',
                top: (presenter.chartInner.height() - presenter.drawingXPosition + Math.round(presenter.axisXLine.height() / 2)) + 'px',
                bottom: ''
            });
        }

        if (isExample) {
            $columnContainer.addClass('graph_column_container_show_example_answers');
            valueContainer.addClass('graph_show_example_answers');
        } else {
            $columnContainer.addClass('graph_column_container_show_answers');
            valueContainer.addClass('graph_show_answers');
        }
    }

    presenter.removeAnswerFromGraph = function (index, element) {
        const currentValue = presenter.currentData[index],
            valueContainer = $(element),
            $columnContainer = valueContainer.parent('').find('.graph_column_container_interactive');

        if(currentValue &gt;= 0) {
            valueContainer.css({
                bottom: (presenter.drawingXPosition - Math.round(presenter.axisXLine.height() / 2)) + 'px',
                height: parseFloat(currentValue / presenter.absoluteRange) * 100 + '%',
                top: ''
            });
        } else if (currentValue &lt; 0) {
            valueContainer.css({
                height: parseFloat(currentValue * -1 / presenter.absoluteRange) * 100 + '%',
                top: (presenter.chartInner.height() - presenter.drawingXPosition + Math.round(presenter.axisXLine.height() / 2)) + 'px',
                bottom: ''
            });
        }
        $columnContainer.removeClass('graph_column_container_show_answers');
        valueContainer.removeClass('graph_show_answers');
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="multiplegap" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property name="Orientation" nameLabel="multiplegap_property_orientation" type="{horizontal, vertical}"/>
		<property name="Source type" nameLabel="multiplegap_property_source_type" type="{images, texts, audio}"/>
		<property name="Items" nameLabel="multiplegap_property_items" type="list">
			<property name="Answer ID" nameLabel="multiplegap_property_answer_id" type="string"/>
		</property>
        <property name="wrapItems" nameLabel="multiplegap_property_wrap_items" type="boolean"/>
		<property name="Item width" nameLabel="multiplegap_property_item_width" type="string"/>
		<property name="Item height" nameLabel="multiplegap_property_item_height" type="string"/>
		<property name="Item spacing" nameLabel="multiplegap_property_item_spacing" type="string"/>
		<property name="Stretch images?" nameLabel="multiplegap_property_stretch_images" type="boolean"/>
		<property name="Item horizontal align" nameLabel="multiplegap_property_item_horizontal_align" type="{none, left, right, center}"/>
		<property name="Item vertical align" nameLabel="multiplegap_property_item_vertical_align" type="{none, top, bottom, center}"/>
		<property name="Maximum item count" nameLabel="multiplegap_property_maximum_item_count" type="string"/>
		<property name="Is not an activity" nameLabel="multiplegap_property_is_not_an_activity" type="boolean"/>
        <property name="Number of repetitions" nameLabel="multiplegap_property_number_of_repetitions" type="string"/>
        <property name="ID repeated element" nameLabel="multiplegap_property_id_repeated_element" type="string"/>
        <property displayName="Block wrong answers" name="Block wrong answers" nameLabel="multiplegap_property_block_wrong_answers" type="boolean"/>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="multiple_property_lang_attribute" type="string"/>
		<property displayName="Speech texts" name="speechTexts" nameLabel="multiplegap_property_speech_texts" type="staticlist">
            <property name="Inserted" nameLabel="multiplegap_property_inserted" type="staticrow">
                <property name="Inserted" nameLabel="multiplegap_property_inserted" type="string"/>
            </property>

            <property name="Removed" nameLabel="multiplegap_property_removed" type="staticrow">
                <property name="Removed" nameLabel="multiplegap_property_removed" type="string"/>
            </property>

			<property name="Empty" nameLabel="multiplegap_property_empty" type="staticrow">
                <property name="Empty" nameLabel="multiplegap_property_empty" type="string"/>
            </property>

			<property name="Correct" nameLabel="multiplegap_property_correct" type="staticrow">
                <property name="Correct" nameLabel="multiplegap_property_correct" type="string"/>
            </property>

			<property name="Wrong" nameLabel="multiplegap_property_wrong" type="staticrow">
                <property name="Wrong" nameLabel="multiplegap_property_wrong" type="string"/>
            </property>
		</property>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
	</model>
<css>.multiplegap_container {
    position: absolute;
}

.multiplegap_container .placeholder {
    position: absolute;
    overflow: hidden;
}

.multiplegap_texts .placeholder img,
.multiplegap_texts .placeholder p
{
    margin: 0;
    padding: 0;
}

.multiplegap_container .handler {
    width: 100%;
    height: 100%;
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
}

.multiplegap_active {
    cursor: pointer;
}

.multiplegap_container .placeholder_valid {
    border: 1px solid #00ff00;
}

.multiplegap_container .placeholder_invalid {
    border: 1px solid #ff0000;
}

.multiplegap_container .placeholder_valid .handler,
.multiplegap_container .placeholder_invalid .handler {
    cursor: default;
}

.printable_addon_multiplegap .correctAnswerSpan::after {
    content: "\2714";
    display: inline-block;
    margin: 3px;
}

.printable_addon_multiplegap .incorrectAnswerSpan::after {
    content: "\2716";
    display: inline-block;
    margin: 3px;
}

.printable_addon_multiplegap .answerSpan {
    display: inline-block;
}
</css><view/><preview/><presenter>function Addonmultiplegap_create(){
    /*
     * KNOWN ISSUES:
     *     Invalid properties values (Item width, Item height, Item spacing, Maximum item count):
     *          When addon validated model it checks only value of those properties by numerical values. If parsed
     *         value is not a number - no error message is showed - that's because calculated values from those
     *         properties are only used in CSS. Invalid CSS value set with jQuery will simply not be added to
     *         DOM element, but it won't brake anything. Changing this behaviour will break backward compatibility!
     */

    function getTextVoiceObject (text, lang) {return {text: text, lang: lang};}
    var isWCAGOn = false;
    var printableController = null;
    var textParser = null;

    var presenter = function(){};

    function getPlaceholders() {
        return jQuery.map(presenter.$view.find('.placeholder:not(.placeholder-show-answers)'), function (placeholder) {
            return {
                item: $(placeholder).attr('draggableItem'),
                value: $(placeholder).attr('draggableValue'),
                type: $(placeholder).attr('draggableType')
            };
        });
    }
    
    presenter.ORIENTATIONS = {
        HORIZONTAL: 0,
        VERTICAL: 1
    };
    
    presenter.SOURCE_TYPES = {
        IMAGES: 0,
        TEXTS: 1,
        AUDIO: 2
    };

    presenter.ERROR_CODES = {
        INVALID_ITEM_WIDTH: "Item width has to be greater than 0",
        INVALID_ITEM_HEIGHT: "Item height has to be greater than 0",
        INVALID_ITEM_SPACING: "Item spacing has to be greater or equal than 0",
        INVALID_MAXIMUM_ITEM_COUNT: "Maximum item count has to be greater or equal than 1",
        INVALID_NUMBER_OF_REPETITION: "Incorrect value. It should be integer and greater than 0",
        INVALID_REPEATED_ELEMENT: "Incorrect value. This field should contains only one ID"
    };
    
    presenter.eventBus            = null;
    presenter.playerController    = null;
    
    presenter.$view               = null;
    
    presenter.selectedItem        = null;
    
    presenter.showErrorsMode      = false;
    presenter.isShowAnswersActive = false;
    presenter.isGradualShowAnswersActive = false;
    presenter.itemCounterMode = false;
    presenter.placeholders2drag = [];

    presenter.keyboardControllerObject = null;
    presenter.container = null;
    presenter.elementCounter = 0;
    presenter.printableState = null;
    
    presenter.createPreview = function(view, model) {
        presenter.createLogic(view, model, true);
    };
    
    presenter.run = function(view, model) {
        presenter.createLogic(view, model, false);
    };
    
    presenter.validateItems = function (model) {
        var itemWidth = parseInt(model['Item width']);
        if (!isNaN(itemWidth) &amp;&amp; itemWidth &lt;= 0) {
            return {isError: true, errorCode: 'INVALID_ITEM_WIDTH'};
        }
        
        var itemHeight = parseInt(model['Item height']);
        if (!isNaN(itemHeight) &amp;&amp; itemHeight &lt;= 0) {
            return {isError: true, errorCode: 'INVALID_ITEM_HEIGHT'};
        }
        
        var itemSpacing = parseInt(model['Item spacing']);
        if (!isNaN(itemSpacing) &amp;&amp; itemSpacing &lt; 0) {
            return {isError: true, errorCode: 'INVALID_ITEM_SPACING'};
        }
        
        var maximumItemCount = parseInt(model['Maximum item count']);
        if (!isNaN(maximumItemCount) &amp;&amp; maximumItemCount &lt; 1) {
            return {isError: true, errorCode: 'INVALID_MAXIMUM_ITEM_COUNT'};
        }
        
        return {
            isError: false,
            value: {
                width: itemWidth,
                height: itemHeight,
                spacing: itemSpacing,
                maximumCount: maximumItemCount,
                horizontalAlign: model['Item horizontal align'],
                verticalAlign: model['Item vertical align']
            }
        };
    };
    
    presenter.validateRepetitions = function(number) {
        if (number === undefined || number === "") {
            number = 0;
        }
        number = parseInt(number, 10);
        if (isNaN(number) || number &lt; 0) {
            return {isError: true, errorCode: 'INVALID_NUMBER_OF_REPETITION'};
        }
        
        return {
            isError: false,
            value: number
        }
    };
    
    presenter.validateIdRepeatedElement = function(id) {
        id = id || "";
        
        if (id.indexOf(",") !== -1 || id.indexOf(";") !== -1) {
            return {isError: true, errorCode: 'INVALID_REPEATED_ELEMENT'};
        }
        
        return {
            isError: false,
            value: id
        }
    };
    
    presenter.upgradeModel = function (model) {
        var upgradedModel = model;
        if (model["wrapItems"] === undefined) {
            upgradedModel = this.upgradeWrapItems(model);
        }
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        return upgradedModel;
    };
    
    presenter.upgradeWrapItems = function (model) {
        model["wrapItems"] = false;
        
        return model;
    };

    presenter.upgradeSpeechTexts = function (model) {
         var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (!upgradedModel["speechTexts"]) {
            upgradedModel["speechTexts"] = {};
        }
        if (!upgradedModel["speechTexts"]['Inserted']) {
            upgradedModel["speechTexts"]['Inserted'] = {Inserted: 'Inserted'};
        }
        if (!upgradedModel["speechTexts"]['Removed']) {
            upgradedModel["speechTexts"]['Removed'] = {Removed: 'Removed'};
        }
        if (!upgradedModel["speechTexts"]['Empty']) {
            upgradedModel["speechTexts"]['Empty'] = {Empty: 'Empty'};
        }
        if (!upgradedModel["speechTexts"]['Correct']) {
            upgradedModel["speechTexts"]['Correct'] = {Correct: 'Correct'};
        }
        if (!upgradedModel["speechTexts"]['Wrong']) {
            upgradedModel["speechTexts"]['Wrong'] = {Wrong: 'Wrong'};
        }
        if (!upgradedModel["langAttribute"]) {
            upgradedModel["langAttribute"] = "";
        }
        return upgradedModel;
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            inserted:  'inserted',
            removed: 'removed',
            empty: 'empty',
            correct: 'correct',
            wrong: 'wrong'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            inserted:    getSpeechTextProperty(speechTexts['Inserted']['Inserted'], presenter.speechTexts.inserted),
            removed: getSpeechTextProperty(speechTexts['Removed']['Removed'], presenter.speechTexts.removed),
            empty: getSpeechTextProperty(speechTexts['Empty']['Empty'], presenter.speechTexts.empty),
            correct: getSpeechTextProperty(speechTexts['Correct']['Correct'], presenter.speechTexts.correct),
            wrong: getSpeechTextProperty(speechTexts['Wrong']['Wrong'], presenter.speechTexts.wrong)
        };
    }
    
    
    presenter.validateModel = function (model) {
        setSpeechTexts(model['speechTexts']);
        var orientation = presenter.ORIENTATIONS.HORIZONTAL;
        if (model['Orientation'] === "vertical") {
            orientation = presenter.ORIENTATIONS.VERTICAL;
        }
        
        var sourceType = presenter.SOURCE_TYPES.IMAGES;
        if (model['Source type'] === "texts") {
            sourceType = presenter.SOURCE_TYPES.TEXTS;
        } else if (model['Source type'] === 'audio') {
            sourceType = presenter.SOURCE_TYPES.AUDIO;
        }
        
        var validatedItems = presenter.validateItems(model);
        if (validatedItems.isError) {
            return validatedItems;
        }
        
        var isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        var validatedRepetitions = presenter.validateRepetitions(model["Number of repetitions"]);
        var validateRepeatedElement = presenter.validateIdRepeatedElement(model["ID repeated element"]);

        var isTabindexEnabled = ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]);
        
        if (validatedRepetitions.isError) {
            return validatedRepetitions;
        }
        if (validateRepeatedElement.isError) {
            return validateRepeatedElement;
        }
        
        return {
            isError: false,
            ID: model.ID,
            isActivity: !ModelValidationUtils.validateBoolean(model['Is not an activity']),
            isVisible: isVisible,
            isVisibleByDefault: isVisible,
            orientation: orientation,
            sourceType: sourceType,
            stretchImages: model['Stretch images?'] === 'True',
            items: validatedItems.value,
            itemsAnswersID: model['Items'].map(function (item) {
                return item['Answer ID'];
            }),
            repetitions: validatedRepetitions.value,
            repeatedElement: validateRepeatedElement.value,
            blockWrongAnswers: ModelValidationUtils.validateBoolean(model["Block wrong answers"]),
            wrapItems: ModelValidationUtils.validateBoolean(model["wrapItems"]),
            isTabindexEnabled: isTabindexEnabled,
            langTag: model['langAttribute']
        };
    };
    
    presenter.destroyCommands = function Multiplegap_destroyCommands() {
        delete presenter.executeCommand;
        delete presenter.countItems;
        delete presenter.isAllOK;
        delete presenter.isAttempted;
        delete presenter.show;
        delete presenter.hide;
        delete presenter.getMaxScore;
        delete presenter.getScore;
        delete presenter.getErrorCount;
        delete presenter.setShowErrorsMode;
        delete presenter.setWorkMode;
        delete presenter.reset;
        delete presenter.getState;
        delete presenter.setState;
    };
    
    presenter.createView = function Multiplegap_createView () {
        var container = $('&lt;div class="multiplegap_container"&gt;&lt;/div&gt;');

        if (this.configuration.isTabindexEnabled) {
            container.attr("tabindex", "0");
        }

        container.click (function (event) {
            event.stopPropagation ();
            event.preventDefault ();
        });
        var placeholders = $('&lt;div class="multiplegap_placeholders"&gt;&lt;/div&gt;');
        
        container.append(placeholders);
        
        container.addClass("multiplegap_" + (presenter.configuration.orientation === presenter.ORIENTATIONS.HORIZONTAL ? "horizontal" : "vertical"));
        container.addClass("multiplegap_" + (presenter.configuration.sourceType === presenter.SOURCE_TYPES.TEXTS ? "texts" : "images" ));
        
        container.css ({
            width: presenter.$view.css ('width'),
            height: presenter.$view.css ('height')
        });
        
        container.click (presenter.acceptDraggable);

        var originalGeneratePosition = null;
        container.droppable ({
            drop: function (event, ui) {
                if (!presenter.configuration.isVisible) {
                    return;
                }
                event.stopPropagation();
                event.preventDefault();
                container.click();
            },
            activate: function (event, ui) {
                if (presenter.$view.find('.dragging').length &gt; 0 &amp;&amp; originalGeneratePosition == null) {
                    originalGeneratePosition = $.ui.draggable.prototype._generatePosition;
                    $.ui.draggable.prototype._generatePosition = scaledOriginalPositionDecorator($.ui.ddmanager.current, originalGeneratePosition);
                }
            },
            deactivate: function (event, ui) {
                if (originalGeneratePosition != null) {
                    $.ui.draggable.prototype._generatePosition = originalGeneratePosition;
                    originalGeneratePosition = null;
                }
            }
        });

        presenter.container = container;
        presenter.$view.append(container);
    };

    function scaledOriginalPositionDecorator(obj, fn) {

        function getScale() {
            if (presenter.playerController) {
                const scaleInformation = presenter.playerController.getScaleInformation();
                if (scaleInformation.baseScaleX !== 1.0 ||
                    scaleInformation.baseScaleY !== 1.0 ||
                    scaleInformation.scaleX !== 1.0 ||
                    scaleInformation.scaleY !== 1.0
                ) {
                    return {X: scaleInformation.scaleX, Y: scaleInformation.scaleY};
                }
            }

            const $content = $("#content");
            if ($content.size() &gt; 0) {
                const contentElem = $content[0];
                const scaleX = contentElem.getBoundingClientRect().width / contentElem.offsetWidth;
                const scaleY = contentElem.getBoundingClientRect().height / contentElem.offsetHeight;
                return {X: scaleX, Y: scaleY};
            } else {
                return {X: 1.0, Y: 1.0};
            }
        }

        obj.isGeneratePositionScaled = true; //add marker to draggable for use in the droppable intersect fix
        return function (event) {
            var scale = getScale();
            var pos = fn.apply(obj, [event]);
            return {left: pos.left / scale.X, top: pos.top / scale.Y};
        }
    }
    
    presenter.setUpEventListeners = function Multiplegap_setUpEventListeners (isPreview) {
        if (!isPreview) {
            presenter.pageLoadedDeferred = new $.Deferred();
            presenter.pageLoaded = presenter.pageLoadedDeferred.promise ();
            
            presenter.eventBus.addEventListener ('ItemSelected', presenter.eventListener);
            presenter.eventBus.addEventListener ('ItemConsumed', presenter.eventListener);
            presenter.eventBus.addEventListener ('PageLoaded', this);
            presenter.eventBus.addEventListener ('ShowAnswers', this);
            presenter.eventBus.addEventListener ('HideAnswers', this);
            presenter.eventBus.addEventListener ('NotAllAttempted', this);
            presenter.eventBus.addEventListener ('Submitted', this);
            presenter.eventBus.addEventListener ('ValueChanged', this);
            presenter.eventBus.addEventListener('GradualShowAnswers', this);
            presenter.eventBus.addEventListener('GradualHideAnswers', this);
        }
    };

    presenter.createLogic = function Multiplegap_createLogic (view, model, isPreview) {
        presenter.$view = $(view);
        presenter.addonID = model.ID;
        
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);
        presenter.setItemCounterModeValue();
        
        if (presenter.configuration.isError) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, presenter.configuration.errorCode);
            presenter.destroyCommands ();
            return;
        }
        
        presenter.setUpEventListeners(isPreview);
        presenter.createView();
        
        if (!presenter.configuration.isVisibleByDefault) {
            presenter.hide();
        }

        presenter.buildKeyboardController();
        presenter.elementCounter = presenter.$view.find('.placeholder').not('.ui-draggable-dragging').length;
    };
    
    presenter.setItemCounterModeValue = function MultipleGap_setItemCounterModeValue () {
        if (presenter.configuration.repetitions !== undefined &amp;&amp;
          presenter.configuration.repeatedElement !== undefined &amp;&amp;
          presenter.configuration.repetitions !== 0 &amp;&amp;
          presenter.configuration.repeatedElement !== "")
        {
            presenter.itemCounterMode = true;
        }
    };
    
    presenter.eventListener = {
        onEventReceived: function(eventName, eventData) {
            if(presenter.showErrorsMode || presenter.isShowAnswersActive) return;

            switch (true) {
                case eventName === "ItemConsumed":
                    presenter.$view.find('.handler').show();
                    presenter.isItemChecked = false;
                    break;

                case eventName === "ItemSelected" &amp;&amp; eventData.value !== null &amp;&amp; eventData.value !== "":
                    presenter.$view.find('.handler').hide();
                    presenter.isItemChecked = true;
                    break;

                case eventName === "ItemSelected" &amp;&amp; eventData.value == null &amp;&amp; presenter.isAllOK():
                    presenter.$view.find('.handler').show();
                    presenter.isItemChecked = false;
                    sendAllOKEvent();
                    break;

                case eventName === "ItemSelected":
                    presenter.$view.find('.handler').show();
                    presenter.isItemChecked = false;
                    break;
            }

            switch (true) {
                case typeof(eventData.item) == "undefined" || eventData.item === null:
                    presenter.clearSelected();
                    break;

                case presenter.configuration.sourceType === presenter.SOURCE_TYPES.IMAGES &amp;&amp; eventData.type === "image":
                case presenter.configuration.sourceType === presenter.SOURCE_TYPES.TEXTS &amp;&amp; eventData.type === "string":
                case presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO &amp;&amp; eventData.type === "audio":
                    presenter.saveSelected(eventData);
                    break;

                default:
                    presenter.clearSelected();
            }
        }
    };
    
    presenter.selectorRootClass = function() {
        switch(presenter.configuration.sourceType) {
            case presenter.SOURCE_TYPES.IMAGES:
                return ".multiplegap_images";
            
            case presenter.SOURCE_TYPES.TEXTS:
                return ".multiplegap_texts";
        }
    };
    
    presenter.clearSelected = function() {
        presenter.selectedItem = null;
        $(presenter.selectorRootClass() + '.multiplegap_active').removeClass('multiplegap_active');
        
        $(presenter.selectorRootClass() + ' .placeholder_inactive').removeClass('placeholder_inactive');
        
        $(presenter.selectorRootClass() + ' .handler_disabled')
          .click(presenter.removeDraggable)
          .removeClass('handler_disabled');
        
        $(presenter.selectorRootClass() + '&gt;.handler').hide();
    };
    
    presenter.saveSelected = function(eventData) {
        presenter.selectedItem = eventData;
        
        if(!presenter.maximumItemCountReached()) {
            presenter.$view.find('.multiplegap_container').addClass('multiplegap_active');
        }
        
        if (!presenter.isItemChecked) {
            presenter.$view.find('.multiplegap_container').removeClass('multiplegap_active');
        }
    };
    
    presenter.acceptDraggable = function(e) {
        e.stopPropagation();
        e.preventDefault();
        
        if(presenter.showErrorsMode || presenter.isShowingAnswers() || !presenter.isItemChecked) {
            return;
        }

        presenter.performAcceptDraggable($(e.target), presenter.selectedItem, true, false, false);
        presenter.$view.find('.handler').show();
        presenter.$view.find('.multiplegap_container').removeClass('multiplegap_active');
        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());

        if (presenter.configuration.isTabindexEnabled) {
            presenter.container.removeAttr("tabindex");
        }
    };
    
    presenter.maximumItemCountReached = function() {
        return presenter.countItems() &gt;= presenter.configuration.items.maximumCount;
    };
    
    presenter.parseItemValue = function (item) {
        if(item.indexOf("**") &gt; -1 || item.indexOf("__") &gt; -1){
            return item.replace(/\*\*(.*?)\*\*/g, "&lt;b&gt;$1&lt;/b&gt;").replace(/__(.*?)__/g, "&lt;i&gt;$1&lt;/i&gt;").replace(/__(.*?)_/g, "&lt;i&gt;$1_&lt;/i&gt;").replace(/\*\*(.*?)\*/g, "&lt;b&gt;$1*&lt;/b&gt;").replace(/_(.*?)__/g, "_$1").replace(/\*(.*?)\*\*/g, "*$1");
        }else{
            return item;
        }
    };
    
    presenter.getImageURL = function (elem) {
        var imageSourceModule = presenter.playerController.getModule(elem.item);
        
        if (imageSourceModule == null || !imageSourceModule.hasOwnProperty('getImageUrl')) {
            return '';
        }

        return imageSourceModule.getImageUrl();
    };

    presenter.updateLaTeX = function (element) {
        const args = [];
        args.push("Typeset", MathJax.Hub, element);
        MathJax.Hub.Queue(args);
    };


    presenter.getAltText = function (id) {
        var imageSourceModule = presenter.playerController.getModule(id);
        if (imageSourceModule == null || !imageSourceModule.hasOwnProperty('getAltText')) {
            return '';
        }
        return imageSourceModule.getAltText();
    };

    presenter.getItemLangAttribute = function (id) {
        var imageSourceModule = presenter.playerController.getModule(id);
        if (imageSourceModule == null || !imageSourceModule.hasOwnProperty('getLangAttribute')) {
            return '';
        }
        return imageSourceModule.getLangAttribute();
    };
    
    presenter.calculateElementPositions = function () {
        var orientation = presenter.configuration.orientation;
        var wrapItems = presenter.configuration.wrapItems;
        var isHorizontalOrientation = orientation === presenter.ORIENTATIONS.HORIZONTAL;
        var isVerticalOrientation = orientation === presenter.ORIENTATIONS.VERTICAL;
        var elementWidth = presenter.configuration.items.width;
        var elementHeight = presenter.configuration.items.height;
        var rowPositions;
        
        var positions;
        if (isHorizontalOrientation &amp;&amp; !wrapItems) {
            positions = {
                left: presenter.calculateSpaceUsedByElements(presenter.configuration.items.width),
                top: 0
            };
        } else if (isVerticalOrientation &amp;&amp; !wrapItems) {
            positions = {
                left: 0,
                top: presenter.calculateSpaceUsedByElements(presenter.configuration.items.height)
            };
        } else if (isHorizontalOrientation &amp;&amp; wrapItems) {
            rowPositions = presenter.calculatePositionsInRow(presenter.getContainerWidth(), elementWidth, elementHeight);
            positions = {
                top: rowPositions.rowPosition,
                left: rowPositions.positionInRow
            };
        } else if (isVerticalOrientation &amp;&amp; wrapItems) {
            rowPositions = presenter.calculatePositionsInRow(presenter.getContainerHeight(), elementHeight, elementWidth);
            positions = {
                top: rowPositions.positionInRow,
                left: rowPositions.rowPosition
            };
        }
        
        return positions;
    };
    
    presenter.calculatePositionsInRow = function (containerSize, elementSize, rowSize, itemsCountFixed) {
        var elementSpacing = presenter.configuration.items.spacing;
        var howManyInRow = presenter.calculateHowManyElementsInContainer(containerSize, elementSize, elementSpacing);
        var countItems = presenter.countItems(itemsCountFixed);
        var row = parseInt(countItems / howManyInRow, 10);
        
        var rowPosition = 0;
        var positionInRow = presenter.calculateSpaceUsedByElements(elementSize, countItems);
        if (row &gt; 0) {
            var itemInRow = countItems - (howManyInRow * row);
            rowPosition = row * (rowSize + elementSpacing);
            positionInRow = itemInRow * (elementSize + elementSpacing);
        }
        
        return {
            rowPosition: rowPosition,
            positionInRow: positionInRow
        };
    };
    
    presenter.calculateHowManyElementsInContainer = function (containerSize, elementSize, spacing) {
        var howManyInContainer = 1;
        var usedSize = elementSize;
        
        while (true) {
            usedSize += spacing;
            usedSize += elementSize;
            
            if (usedSize &lt;= containerSize) {
                howManyInContainer++;
            } else {
                break;
            }
        }
        
        return howManyInContainer;
    };
    
    presenter.calculateSpaceUsedByElements = function (elementSize, itemsToCount) {
        return presenter.countItems(itemsToCount) * (elementSize + presenter.configuration.items.spacing);
    };
    
    presenter.getContainerWidth = function () {
        return presenter.$view.width();
    };
    
    presenter.getContainerHeight = function () {
        return presenter.$view.height();
    };

    presenter.performAcceptDraggable = function(handler, item, sendEvents, force, isState) {
        if(!presenter.isShowingAnswers()){
            if(!force &amp;&amp; presenter.selectedItem == null) return;
            if(presenter.maximumItemCountReached()) return;
            if(presenter.configuration.blockWrongAnswers &amp;&amp; !presenter.isElementCorrect(item.item)) {
                if (sendEvents) {
                    sendEvent(item, false);
                }
                return;
            }
        }

        const placeholder = presenter.createDraggablePlaceholderBase(item);
        const child = presenter.createDraggableItem(item);

        presenter.$view.find('.multiplegap_placeholders').append(placeholder);
        presenter.elementCounter++;
        placeholder.append(child);

        if (!isState) {
            presenter.updateLaTeX(child[0]);
        }

        presenter.positionDraggableItem(placeholder, child);

        handler = $('&lt;div class="handler" style="color: rgba(0,0,0,0.0); font-size:1px"&gt;' + presenter.getAltText(item.item) + '&lt;/div&gt;');
        
        // Workaround for IE bug: empty divs in IE are not clickable so let's
        // make them not empty and appear as empty.
        if($.browser.msie) {
            handler.css({backgroundColor: "#000000", opacity: 0 });
        }

        // If the source type is audio, the handler should only cover the grab area and leave the button uncovered
        if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO) {
            handler.css('width','50px');
        }
        
        handler.click(presenter.removeDraggable);
        placeholder.append(handler);
        
        if(sendEvents) {
            sendEvent(item, true);
        }
        
        $(presenter.selectorRootClass() + '&gt;.handler').hide();
        
        presenter.clearSelected();
        
        if (isState) {
            presenter.placeholders2drag.push(placeholder);
        } else {
            presenter.makePlaceholderDraggable(placeholder);
        }

        if(isWCAGOn) {
            presenter.handleDraggableItemTTS(child)
        }
    };

    presenter.createDraggablePlaceholderBase = function(item) {
        const placeholder = presenter.isShowingAnswers()
            ? $('&lt;div class="placeholder placeholder-show-answers"&gt;&lt;/div&gt;')
            : $('&lt;div class="placeholder"&gt;&lt;/div&gt;')

        const positions = presenter.calculateElementPositions();
        placeholder.css({
            top: positions.top + 'px',
            left: positions.left + 'px',
            width: presenter.configuration.items.width + 'px',
            height: presenter.configuration.items.height + 'px'
        });

        placeholder.attr({
              draggableValue: item.value,
              draggableItem: item.item,
              draggableType: item.type
          })

        if (presenter.configuration.isTabindexEnabled) {
            placeholder.attr("tabindex", "0");
        }

        return placeholder;
    };

    presenter.createDraggableItem = function(item) {
        const sourceType = presenter.configuration.sourceType;
        const draggableItemConstructorMap = {
            [presenter.SOURCE_TYPES.TEXTS]: presenter.createDraggableTextItem,
            [presenter.SOURCE_TYPES.IMAGES]: presenter.createDraggableImageItem,
            [presenter.SOURCE_TYPES.AUDIO]: createDraggableAudioItem,
        }

        return draggableItemConstructorMap[sourceType](item);
    };

    presenter.createDraggableTextItem = function(item) {
        var child = $('&lt;p class="contents"&gt;&lt;/p&gt;');
        var parsedValue = item.value;
        if (parsedValue.match(/&lt;[^ ].*&gt;/) == null) {
            parsedValue = item.value.replace('&lt;', '&lt; ');
        }
        child.html(presenter.parseItemValue(parsedValue));
        child.html(textParser.parseAltTexts(child.html()));

        return child;
    };

    presenter.createDraggableImageItem = function(item) {
        const child = $(`&lt;img class="contents" alt="${presenter.getAltText(item.item)}" lang="${presenter.getItemLangAttribute(item.item)}" /&gt;`);
        child.attr('src', presenter.getImageURL(item));

        if(presenter.configuration.stretchImages) {
            child.css({
                width: presenter.configuration.items.width + 'px',
                height: presenter.configuration.items.height + 'px'
            });
        }

        return child;
    };

    function createDraggableAudioItem (item) {
        const itemID = item.item
        var $el = $('&lt;div&gt;&lt;/div&gt;');

        var addonAndItemIds = itemID.split('-');
        if (addonAndItemIds.length !== 2) return;
        var audioAddonID = addonAndItemIds[0];
        var audioItemID = addonAndItemIds[1];

        $el.attr('data-audio-id', audioItemID);
        $el.attr('data-addon-id', audioAddonID);
        $el.addClass('multiaudio-item-wrapper');

        var $grab = $('&lt;div&gt;&lt;/div&gt;');
        $grab.addClass('multiaudio-item-grab-area');
        $el.append($grab);

        var audioAddon = presenter.playerController.getModule(audioAddonID);
        if (audioAddon != null) {
            var itemText = audioAddon.getTextFromFileID(audioItemID);
            itemText = textParser.parseAltTexts(itemText);
            if (itemText !== null &amp;&amp; $("&lt;span&gt;"+itemText+"&lt;/span&gt;").text().length &gt; 0) {
                var $text = $('&lt;span&gt;&lt;/span&gt;');
                $text.addClass('multiaudio-item-text');
                $text.html(itemText);
                $grab.append($text);
                $el.addClass("multiaudio-item-has-text");
            }
        }

        var $button = $('&lt;div&gt;&lt;/div&gt;');
        $button.addClass('multiaudio-item-button');
        $el.append($button);

        var $icon = $('&lt;div&gt;&lt;/div&gt;');
        $icon.addClass('multiaudio-item-icon');
        $button.append($icon);

        $button.click(function (event) {
            var $parent = $(event.currentTarget).parent();
            playDraggableAudio($parent, audioItemID, audioAddonID);
        });

        return $el;
    };

    function playDraggableAudio($parent, itemID, audioAddonID) {
        var audioAddon = presenter.playerController.getModule(audioAddonID);
        if ($parent.hasClass('playing')) {
            $parent.removeClass('playing');
            audioAddon.stop();
            audioAddon.jumpToID(itemID);
        } else {
            $parent.addClass('playing');
            audioAddon.jumpToID(itemID);
            audioAddon.play();
        }
    }

    function stopDraggableAudioOnRemove(helper, draggableItem) {
        if (presenter.configuration.sourceType !== presenter.SOURCE_TYPES.AUDIO) return;

        var addonAndItemIds = draggableItem.split('-');
        if (addonAndItemIds.length !== 2) return;

        var audioAddonID = addonAndItemIds[0];
        var audioAddon = presenter.playerController.getModule(audioAddonID);

        var itemID = addonAndItemIds[1];

        helper.find('.playing').each(function(){ //there should be no more than one such element
            var $this = $(this);
            $this.removeClass('playing');
            audioAddon.stop();
            audioAddon.jumpToID(itemID);
        })
    }

    presenter.positionDraggableItem = function(placeholder, item) {
        presenter.applyDraggableItemHorizontalPositioning(placeholder, item);
        presenter.applyDraggableItemVerticalPositioning(placeholder, item);
    };

    presenter.applyDraggableItemHorizontalPositioning = function(placeholder, item) {
        const sourceType = presenter.configuration.sourceType;
        const placeholderPadding = DOMOperationsUtils.getOuterDimensions(placeholder).padding;
        const placeholderHorizontalPadding = placeholderPadding.top + placeholderPadding.bottom;
        const horizontalAlignType = presenter.configuration.items.horizontalAlign;

        const centerTypeStylesBasedOnSourceTypeMap = {
            [presenter.SOURCE_TYPES.TEXTS]: { position: 'absolute', width: '100%', textAlign: 'center' },
            [presenter.SOURCE_TYPES.IMAGES]: { position: 'absolute', left: Math.round((presenter.configuration.items.width - placeholderHorizontalPadding - parseInt(item.css('width'))) / 2) + 'px'},
        }

        switch(horizontalAlignType) {
            case 'center':
                item.css(centerTypeStylesBasedOnSourceTypeMap[sourceType])
                break;
            case 'right':
            case 'left':
                item.css({
                    position: 'absolute',
                    [horizontalAlignType]: 0
                });
                break;
        }
    }

    presenter.applyDraggableItemVerticalPositioning = function(placeholder, item) {
        const placeholderPadding = DOMOperationsUtils.getOuterDimensions(placeholder).padding;
        const placeholderVerticalPadding = placeholderPadding.left + placeholderPadding.right;
        const verticalAlignType = presenter.configuration.items.verticalAlign;

        switch(verticalAlignType) {
            case 'center':
                item.css({
                    position: 'absolute',
                    top: Math.round((presenter.configuration.items.height - placeholderVerticalPadding - parseInt(item.css('height'))) / 2) + 'px'
                });
                break;
            case 'top':
            case 'bottom':
                item.css({
                    position: 'absolute',
                    [verticalAlignType]: 0
                });
                break;
        }
    }

    presenter.handleDraggableItemTTS = function(child) {
        var altText = "";
        var langTag = "";
        var voicesArray = [];
        voicesArray.push(getTextVoiceObject(presenter.speechTexts.inserted));
        if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO) {
            voicesArray = voicesArray.concat(window.TTSUtils.getTextVoiceArrayFromElement(child, presenter.configuration.langTag));
        } else {
            if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.IMAGES) {
                altText = child.attr('alt');
                langTag = child.attr('lang');
                voicesArray.push(getTextVoiceObject(altText,langTag));
            } else {
                // presenter.SOURCE_TYPES.TEXTS
                voicesArray = voicesArray.concat(window.TTSUtils.getTextVoiceArrayFromElement(child, presenter.configuration.langTag));
            }
        }
        presenter.speak(voicesArray);
    }
    
    function sendEvent(item, consumed) {
        if (consumed) {
            presenter.eventBus.sendEvent('ItemConsumed', item);
        }
        
        var score;
        if (presenter.isElementCorrect(item.item)){
            score = 1;
        } else{
            score = 0;
        }

        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item'  : '' + item.item, // ensure that we send string
            'value' : 'add',
            'score' : score
        });
        
        if (presenter.isAllOK()) sendAllOKEvent();
    }
    
    presenter.isElementCorrect = function (item) {
        return presenter.configuration.itemsAnswersID.indexOf(item) &gt; -1;
    };
    
    presenter.makePlaceholderDraggable = function(placeholder) {
        placeholder.draggable({
            revert : false,
            helper: function() {
                if (!presenter.isDragPossible()) {
                    return $('&lt;div&gt;&lt;/div&gt;');
                }
                placeholder.addClass('dragging');
                
                presenter.itemDragged(placeholder);
                return getDraggedSrc(placeholder).clone().show();
            },
            cursorAt: calculateCursorPosition(placeholder),
            appendTo: getDraggedSrc(placeholder) === placeholder ? 'parent' : placeholder.parents('.ic_page:first'),
            start : function(event, ui) {
                if (!presenter.isDragPossible()) {
                    event.stopPropagation();
                    event.preventDefault();
                    return;
                }
                ui.helper.zIndex(100);

            },
            stop : function(event, ui) {
                placeholder.removeClass('dragging');
                ui.helper.zIndex(0);
                ui.helper.remove();
                presenter.itemStopped(placeholder);
                ui.helper.remove();
            }
        });
    };
    
    var getDraggedSrc = function(placeholder) {
        var isAudioGap = presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO;
        if (placeholder.parents('.multiplegap_container').css("overflow") === "hidden" &amp;&amp; !isAudioGap) {
            if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.IMAGES) {
                return $("[id='" + placeholder.attr('draggableitem') + "']");
            } else {
                var item_id = placeholder.attr('draggableitem');
                var container_id = item_id.replace(/\-[0-9]+$/, '');
                return $(presenter.playerController.getModule(container_id).getItemView(item_id));
            }
        } else {
            return placeholder;
        }
    };
    
    var calculateCursorPosition = function(placeholder) {
        var obj = getDraggedSrc(placeholder);
        var position;
        if (obj === placeholder) {
            position = {
                left: Math.round(obj.outerWidth() / 2),
                top: Math.round(obj.outerHeight() / 2)
            };
        } else {
            var src = obj.clone();
            src.width(obj.width());
            src.height(obj.height());
            presenter.$view.parents('.ic_page:first').append(src);
            position = {
                left:  Math.round(src.outerWidth()/2),
                top: Math.round(src.outerHeight()/2)
            };
            src.remove();
        }
        return position;
    };
    
    presenter.isDragPossible = function() {
        if (presenter.showErrorsMode || presenter.isShowingAnswers()) {
            return false;
        }
        return true;
    };
    
    presenter.itemDragged = function(placeholder) {
        var evnt = {
            source: presenter.configuration.ID,
            value: placeholder.attr('draggableValue'),
            item: placeholder.attr('draggableItem'),
            type: placeholder.attr('draggableType')
        };
        presenter.performRemoveDraggable(placeholder.find('.handler'), true);
        presenter.eventBus.sendEvent('itemDragged', evnt);
    };
    
    presenter.itemStopped = function(placeholder) {
        var value = placeholder.attr('draggableValue') + '';
        var item = placeholder.attr('draggableItem') + '';
        var type = placeholder.attr('draggableType') + '';
        placeholder.remove();

        var evnt = {
            source: presenter.configuration.ID,
            value: value,
            item: item,
            type: type
        };
        presenter.eventBus.sendEvent('itemStopped', evnt);
        presenter.$view.find('.placeholder').each(presenter.movePlaceholdersAfterRemove);
    };
    
    presenter.removeDraggable = function(e) {
        e.stopPropagation();
        e.preventDefault();
        
        if(presenter.showErrorsMode || presenter.isShowingAnswers()) {
            return;
        }
        presenter.performRemoveDraggable($(e.target));
        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());

        if(presenter.configuration.isTabindexEnabled &amp;&amp; presenter.$view.find('.placeholder').length === 0) {
            presenter.container.attr("tabindex", "0");
        }
    };
    
    presenter.performRemoveDraggable = function(handler) {
        presenter.elementCounter--;
        var placeholder = handler.parent();
        var child = placeholder.find('.contents, .multiaudio-item-text');
        if(isWCAGOn) {
            var altText = "";
            var langTag = "";
            var voicesArray = [];
            voicesArray.push(getTextVoiceObject(presenter.speechTexts.removed));
            if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.IMAGES) {
                altText = child.attr('alt');
                langTag = child.attr('lang');
                voicesArray.push(getTextVoiceObject(altText,langTag));
            } else {
                voicesArray = voicesArray.concat(window.TTSUtils.getTextVoiceArrayFromElement(child, presenter.configuration.langTag));
            }
            presenter.speak(voicesArray);
        }

        if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO) {
            stopDraggableAudioOnRemove(placeholder, placeholder.attr('draggableitem'));
        }

        if (arguments[1]) {
            placeholder.hide();
            if (placeholder.is(":visible")) {
                placeholder.style("display", "none", "important");
            }
        } else {
            placeholder.remove();
        }
        
        presenter.$view.find('.placeholder').each(presenter.movePlaceholdersAfterRemove);
        
        presenter.eventBus.sendEvent('ItemReturned', {
            value: placeholder.attr('draggableValue'),
            item: placeholder.attr('draggableItem'),
            type: placeholder.attr('draggableType')
        });
        
        presenter.eventBus.sendEvent('ValueChanged', {
            'source': presenter.configuration.ID,
            'item'  : '' + placeholder.attr('draggableItem'), // ensure that we send string
            'value' : 'remove',
            'score' : '0'
        });

        if (presenter.isAllOK()) sendAllOKEvent();
    };
        
    presenter.movePlaceholdersAfterRemove = function (index, element) {
        var orientation = presenter.configuration.orientation;
        var wrapItems = presenter.configuration.wrapItems;
        var isHorizontalOrientation = orientation === presenter.ORIENTATIONS.HORIZONTAL;
        var isVerticalOrientation = orientation === presenter.ORIENTATIONS.VERTICAL;
        var positions;
        var rowPositions;
        var elementWidth = presenter.configuration.items.width;
        var elementHeight = presenter.configuration.items.height;
        
        if (isHorizontalOrientation &amp;&amp; !wrapItems) {
            positions = {
                left: presenter.calculateSpaceUsedByElements(presenter.configuration.items.width, index),
                top: 0
            };
        } else if (isVerticalOrientation &amp;&amp; !wrapItems) {
            positions = {
                left: 0,
                top: presenter.calculateSpaceUsedByElements(presenter.configuration.items.height, index)
            };
        } else if (isHorizontalOrientation &amp;&amp; wrapItems) {
            rowPositions = presenter.calculatePositionsInRow(presenter.getContainerWidth(), elementWidth, elementHeight, index);
            positions = {
                top: rowPositions.rowPosition,
                left: rowPositions.positionInRow
            };
            
        } else if (isVerticalOrientation &amp;&amp; wrapItems) {
            rowPositions = presenter.calculatePositionsInRow(presenter.getContainerHeight(), elementHeight, elementWidth, index);
            positions = {
                top: rowPositions.positionInRow,
                left: rowPositions.rowPosition
            };
        }
        
        $(element).css({
            left: positions.left + 'px',
            top: positions.top + 'px'
        });
    };

    presenter.setPlayerController = function(controller) {
        presenter.playerController = controller;

        textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        textParser = new TextParserProxy(getTextParser());
    };

    presenter.setEventBus = function (eventBus) {
        presenter.eventBus = eventBus;
    };
    
    function sendAllOKEvent () {
        var eventData = {
            'source': presenter.addonID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    }
    
    function getItemsLength(items) {
        return (items.length === 1 &amp;&amp; items[0] === '') ? 0 : items.length;
    }
    
    presenter.getMaxScore = function() {
        if (presenter.itemCounterMode) {
            return presenter.configuration.isActivity ? 1 : 0;
        }
        
        return presenter.configuration.isActivity ? getItemsLength(presenter.configuration.itemsAnswersID) : 0;
    };
    
    function getItemsCount() {
        return presenter.$view.find('.placeholder').not('.dragging').length;
    }
    
    function isAllCorrect () {
        return getItemsCount() === presenter.configuration.repetitions;
    }

    presenter.isShowingAnswers = function() {
        return presenter.isShowAnswersActive || presenter.isGradualShowAnswersActive;
    }
    
    presenter.getScore = function() {
        if (presenter.isShowingAnswers()) {
            return presenter.tmpScore;
        }

        if (presenter.itemCounterMode) {
            var score = isAllCorrect() ? 1 : 0;

            return presenter.configuration.isActivity ? score : 0;
        }

        if (!presenter.configuration.isActivity) return 0;

        var invalidAndRedundantItems = presenter.getInvalidItems();
        var invalidItems = invalidAndRedundantItems.remainingItems;

        return presenter.configuration.itemsAnswersID.length - invalidItems.length;

    };
    
    presenter.getErrorCount = function() {
        if (presenter.isShowingAnswers()) {
            return presenter.tmpErrorCount;
        }

        if (presenter.itemCounterMode) {
            var isEmpty = getItemsCount() === 0,
              result = 0;
            
            if (!isAllCorrect() &amp;&amp; !isEmpty) {
                result = 1;
            }

            return presenter.configuration.isActivity ? result : 0;
        }

        return presenter.configuration.isActivity ? presenter.countItems() - presenter.getScore() : 0;
    };
    
    presenter.isAllOK = function() {
        if (!presenter.configuration.isActivity) return;
        
        if (presenter.itemCounterMode) {
            return isAllCorrect() ? 1 : 0;
        }

        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };
    
    presenter.getInvalidItems = function() {
        var remainingItems = presenter.configuration.itemsAnswersID.slice(0), currentItem;
        var redundantItems = [];

        presenter.$view.find('.placeholder').not('.dragging').each(function(index, placeholder) {
            currentItem = $(placeholder).attr('draggableItem');
            var currentItemIndex = remainingItems.indexOf(currentItem);
            
            if (currentItemIndex !== -1) {
                remainingItems.splice(currentItemIndex, 1);
            } else {
                redundantItems.push(currentItem);
            }
        });

        return {remainingItems: remainingItems, redundantItems: redundantItems};
    };
    
    function markInactive () {
        presenter.$view.find('.multiplegap_container').addClass('multiplegap_inactive');
    }
    
    function removeInactivityMark () {
        presenter.$view.find('.multiplegap_container').removeClass('multiplegap_inactive');
    }
    
    presenter.setShowErrorsMode = function() {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }
        presenter.showErrorsMode = true;
        markInactive();
        if (!presenter.configuration.isActivity) return;
        
        if (presenter.itemCounterMode) {
            if (isAllCorrect()) {
                presenter.$view.find('.multiplegap_placeholders').addClass('placeholder_valid');
            } else if (getItemsCount() === 0) {
                presenter.$view.find('.multiplegap_container').addClass('multiplegap_inactive');
            } else {
                presenter.$view.find('.multiplegap_placeholders').addClass('placeholder_invalid');
            }
        } else {
            var remainingItems = presenter.configuration.itemsAnswersID.slice(0), currentItem;
            presenter.$view.find('.placeholder').each(function(index, placeholder) {
                currentItem = $(placeholder).attr('draggableItem');
                var currentItemIndex = remainingItems.indexOf(currentItem);
                if (currentItemIndex !== -1) {
                    remainingItems.splice(currentItemIndex, 1);
                    $(placeholder).addClass('placeholder_valid');
                }
            });
            
            presenter.$view.find('.placeholder:not(.placeholder_valid)').addClass('placeholder_invalid');
        }
    };
    
    presenter.setWorkMode = function() {
        presenter.showErrorsMode = false;
        removeInactivityMark();
        if (!presenter.configuration.isActivity) return;
        
        presenter.$view.find('.placeholder_valid').removeClass('placeholder_valid');
        presenter.$view.find('.placeholder_invalid').removeClass('placeholder_invalid');
        
    };
    
    presenter.reset = function() {
        if(presenter.isShowAnswersActive){
            presenter.hideAnswers();
        }
        
        presenter.$view.find('.placeholder').remove();
        
        presenter.setWorkMode();
        
        presenter.clearSelected();
        
        presenter.handleDisplayingAddon();
        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
        presenter.elementCounter = 0;
    };

    presenter.isVisible = function() {
        return presenter.$view.css("visibility") === 'visible';
    }

    presenter.handleDisplayingAddon = function () {
        if (presenter.configuration.isVisibleByDefault &amp;&amp; presenter.isVisible()) {
            presenter.show();
        } else {
            presenter.hide();
        }
    }
    
    presenter.getState = function() {
        function getBaseState(placeholders) {
            return JSON.stringify({
                placeholders: placeholders,
                isVisible: presenter.configuration.isVisible
            });
        }

        if (presenter.isShowingAnswers()) {
            return getBaseState(this.tmpState);
        } else {
            return getBaseState(getPlaceholders());
        }
    };
    
    presenter.upgradeState = function (parsedState) {
        return presenter.upgradeStateForVisibility(parsedState);
    };
    
    presenter.upgradeStateForVisibility = function (parsedState) {
        if (parsedState.constructor === Array) {
            // Before introducing show and hide commands, whole state was an array of
            // entered by user elements (called placeholders).
            return {
                placeholders: parsedState,
                isVisible: true
            };
        }
        
        return parsedState;
    };
    
    presenter.setState = function(state) {
        if (!state) {
            return;
        }

        var parsedState = JSON.parse(state),
          upgradedState = presenter.upgradeState(parsedState);

        for (let i = 0; i &lt; upgradedState.placeholders.length; i++) {
            presenter.performAcceptDraggable(presenter.$view.find('.multiplegap_container&gt;.handler'), upgradedState.placeholders[i], false, true, true);
        }

        if (upgradedState.isVisible) {
            presenter.show();
        } else {
            presenter.hide();
        }
        
        presenter.pageLoaded.then(function() {
            for (let i=0; i&lt;presenter.placeholders2drag.length; i++) {
                const placeholder = presenter.placeholders2drag[i];
                presenter.makePlaceholderDraggable(placeholder);
            }
            presenter.updateLaTeX(presenter.getContainerElement());
        });

        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
    };
    
    presenter.getContainerElement = function () {
        return presenter.$view.find('.multiplegap_container')[0];
    };
    
    presenter.countItems = function(itemsToCount) {
        if (itemsToCount !== null &amp;&amp; itemsToCount !== undefined) {
            return itemsToCount;
        }
        return presenter.elementCounter;
    };
    
    presenter.isAttemptedCommand = function() {
        return presenter.isAttempted();
    };
    
    presenter.isAttempted = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        return presenter.countItems() &gt; 0;
    };
    
    presenter.setVisibility = function (isVisible) {
        presenter.$view.css('visibility', isVisible ? 'visible' : 'hidden');
    };
    
    presenter.hide = function () {
        if (presenter.configuration.isVisible) {
            presenter.setVisibility(false);
            presenter.configuration.isVisible = false;
        }
    };
    
    presenter.show = function () {
        if (!presenter.configuration.isVisible) {
            presenter.setVisibility(true);
            presenter.configuration.isVisible = true;
            presenter.updateLaTeX(presenter.getContainerElement());
        }
    };
    
    presenter.executeCommand = function(name, params) {
        var commands = {
            'countItems': presenter.countItems,
            'isAllOK': presenter.isAllOK,
            'isAttempted' : presenter.isAttempted,
            'show': presenter.show,
            'hide': presenter.hide
        };
        
        return Commands.dispatch(commands, name, params, presenter);
    };
    
    presenter.onEventReceived = function(eventName, eventData) {

        if (eventName === 'PageLoaded') {
            presenter.pageLoadedDeferred.resolve();
        } else if (eventName === "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName === "HideAnswers" || eventName === "NotAllAttempted" || eventName === "Submitted") {
            presenter.hideAnswers();
        } else if (eventName === "ValueChanged") {
            presenter.onValueChanged(eventData);
        } else if (eventName === 'GradualShowAnswers') {
            if (!presenter.isGradualShowAnswersActive) {
                presenter.isGradualShowAnswersActive = true;

                presenter.getTemporaryData();
                setUpResetOnce();
            }

            if (presenter.configuration.ID === eventData.moduleID) {
                presenter.resetOnce();
                presenter.gradualShowAnswers(parseInt(eventData.item, 10));
            }
        } else if (eventName === 'GradualHideAnswers') {
            presenter.gradualHideAnswers();
        }
    };

    presenter.onValueChanged = function (eventData) {
        if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO) {
                presenter.$view.find(".multiaudio-item-wrapper").each(function(){
                    var $this = $(this);
                    if ($this.attr('data-addon-id') === eventData.source) {
                        if ($this.attr('data-audio-id') === eventData.item) {
                            if (eventData.value === "00:00" || eventData.value === "pause") {
                                $this.removeClass('playing');
                            } else if (eventData.value === "playing") {
                                $this.addClass('playing');
                            }
                        } else {
                            if (eventData.value === "playing") {
                                $this.removeClass('playing');
                            }
                        }
                    }
                });
            }
    }

    presenter.getElementText = function (id, element) {
        var module = presenter.playerController.getModule(id);

        if (module == null || !module.hasOwnProperty('getItem')) {
            return '';
        }

        return module.getItem(element);
    };

    presenter.getTemporaryData = function() {
        presenter.tmpState = getPlaceholders();
        presenter.tmpScore = presenter.getScore();
        presenter.tmpErrorCount = presenter.getErrorCount();
    }

    presenter.showAnswers = function () {
        if (!presenter.configuration.isActivity) return;

        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }
        presenter.setWorkMode();

        presenter.getTemporaryData();

        presenter.isShowAnswersActive = true;

        presenter.$view.find('.placeholder').remove();
        var iteratedObject = presenter.getActivitiesCount();

        presenter.elementCounter = 0;
        for (var i = 0; i &lt; iteratedObject; i++) {
            presenter.showAnswer(i);
        }

        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
    };

    presenter.showAnswer = function (item) {
        var moduleID;
        var value = '';

        if (presenter.itemCounterMode) {
            moduleID = presenter.configuration.repeatedElement;
        } else {
            moduleID = presenter.configuration.itemsAnswersID[item];
        }

        if (presenter.configuration.sourceType !== presenter.SOURCE_TYPES.IMAGES) {
            var elementId = moduleID.split('-')[0],
              elementIndex = moduleID.split('-')[1];

            value = presenter.getElementText(elementId, elementIndex);
        }

        presenter.performAcceptDraggable('&lt;div&gt;&lt;/div&gt;', {type:'string', value: value, item: moduleID}, false, false, false);
    }

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive &amp;&amp; !presenter.isGradualShowAnswersActive) {
            return;
        }

        presenter.$view.find('.placeholder-show-answers').remove();

        if (presenter.tmpState){
            presenter.elementCounter = 0;
            for (var i = 0; i &lt; presenter.tmpState.length; i++) {
                presenter.performAcceptDraggable(presenter.$view.find('.multiplegap_container&gt;.handler'), presenter.tmpState[i], false, false, false);
            }
        }
        presenter.$view.find('.placeholder-show-answers').removeClass('placeholder-show-answers');
        presenter.isShowAnswersActive = false;
        presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
    };

    presenter.buildKeyboardController = function () {
        presenter.keyboardControllerObject = new MultipleGapKeyboardController(presenter.getElementsForKeyboardNavigation(), 1);
    };

    presenter.getElementsForKeyboardNavigation = function () {
        return $.merge($(presenter.container), presenter.$view.find('.placeholder:visible').not('.ui-draggable-dragging'));
    };

    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        presenter.keyboardControllerObject.handle(keycode, isShiftKeyDown, event)
    };

    function MultipleGapKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    MultipleGapKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    MultipleGapKeyboardController.prototype.constructor = MultipleGapKeyboardController;


    MultipleGapKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        if (willBeClicked) {
            var handler = $(element).children(".handler");
            if (handler.length &gt; 0) {
                return handler;
            }
        }

        return $(element);
    };

    MultipleGapKeyboardController.prototype.switchElement = function (move) {
        if (this.keyboardNavigationElementsLen &lt; 2) {
            presenter.speak([getTextVoiceObject(presenter.speechTexts.empty)]);
            this.markCurrentElement(0);
        } else {
            var new_position_index = this.keyboardNavigationCurrentElementIndex + move;
            if (new_position_index &gt;= this.keyboardNavigationElementsLen) {
                new_position_index = this.keyboardNavigationElementsLen - 1;
            } else if ( new_position_index &lt; 1) {
                new_position_index = 1;
            }
            this.markCurrentElement(new_position_index);
            this.readActiveElement(new_position_index);
        }
    };

    MultipleGapKeyboardController.prototype.readActiveElement = function (index) {
        var voicesArray = [];
        if(index === 0 ){
            voicesArray.push(getTextVoiceObject(presenter.speechTexts.empty));
        } else {
            var $element = this.getTarget(this.keyboardNavigationElements[index]);
            voicesArray = voicesArray.concat(presenter.getTextVoicesFromPlaceholder($element));
        }
        presenter.speak(voicesArray);
    };

    MultipleGapKeyboardController.prototype.enter = function (event) {
        window.KeyboardController.prototype.enter.call(this, event);
        if(this.keyboardNavigationElementsLen &gt; 1) {
            if (presenter.configuration.sourceType === presenter.SOURCE_TYPES.AUDIO) {
                var $element = presenter.$view.find('.placeholder.keyboard_navigation_active_element');
                if ($element.length === 0) {
                    readAllElements();
                } else {
                    var draggableItemValues = $element.attr('draggableitem').split('-');
                    var $wrapper = $element.find('.multiaudio-item-wrapper');
                    var callback = function() {
                        if (draggableItemValues.length === 2) {
                            playDraggableAudio($wrapper, draggableItemValues[1], draggableItemValues[0]);
                        }
                    }
                    var voicesArray = presenter.getTextVoicesFromPlaceholder($element);
                    if (voicesArray.length === 0 || !isWCAGOn) {
                        callback();
                    } else {
                        if ($wrapper.hasClass('playing')) {
                            callback();
                        } else {
                            presenter.speakWithCallback(voicesArray, callback);
                        }
                    }
                }
            } else {
                readAllElements();
            }
        } else {
            presenter.speak([getTextVoiceObject(presenter.speechTexts.empty)]);
        }
    };

    function readAllElements() {
        var voicesArray = [];
        var kc = presenter.keyboardControllerObject;
        for(var i  = 1; i &lt;kc.keyboardNavigationElementsLen; i++) {
            var $element = kc.getTarget(kc.keyboardNavigationElements[i]);
            voicesArray = voicesArray.concat(presenter.getTextVoicesFromPlaceholder($element));
        }
        presenter.speak(voicesArray);
    }

    presenter.setWCAGStatus = function (isOn) {
        isWCAGOn = isOn;
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.getTextVoicesFromPlaceholder = function($placeholder) {
        var voicesArray =[];

        if ($placeholder.length===0) return [];

        if ($placeholder.hasClass('placeholder')) {
            var $child = $placeholder.find("img.contents");
            if ($child.length &gt; 0) {
                voicesArray.push(getTextVoiceObject($child.attr('alt'),$child.attr('lang')));
            } else {
                $child = $placeholder.find("p.contents");
                if ($child.length &gt; 0){
                    voicesArray = window.TTSUtils.getTextVoiceArrayFromElement($child, presenter.configuration.langTag);
                } else {
                    $child = $placeholder.find('.multiaudio-item-text');
                    if ($child.length &gt; 0){
                        voicesArray = window.TTSUtils.getTextVoiceArrayFromElement($child, presenter.configuration.langTag);
                    }
                }
            }
            if ($placeholder.hasClass('placeholder_invalid')) {
                voicesArray.push(getTextVoiceObject(presenter.speechTexts.wrong));
            } else if ($placeholder.hasClass('placeholder_valid')) {
                voicesArray.push(getTextVoiceObject(presenter.speechTexts.correct));
            }
        }
        return voicesArray;
    };

    presenter.speak = function (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; isWCAGOn) {
            tts.speak(data);
        }
    };

    presenter.speakWithCallback = function (data, callback) {
            var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
            if (tts &amp;&amp; isWCAGOn) {
                tts.speakWithCallback(data, callback);
            }
        };

    presenter.setPrintableController = function (controller) {
        printableController = controller;

        textParser = new TextParserProxy(printableController.getTextParser());
    };

    presenter.setPrintableState = function (state){
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }
        presenter.printableState = JSON.parse(state);
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        var upgradedModel = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(upgradedModel);

        var $view = $("&lt;div&gt;&lt;/div&gt;");
        $view.attr("id", model.ID);
        $view.addClass("printable_addon_multiplegap");

        var $wrapper = $("&lt;div&gt;&lt;/div&gt;");
        $wrapper.css("max-width", model.Width+"px");
        $wrapper.css("min-height", model.Height+"px");
        $wrapper.css("border", "1px solid");
        $wrapper.css("padding", "5px");

        if (printableController != null) {
            var correctAnswers = getCorrectAnswers();
            var answerHTML = "";

            if (presenter.printableState) {
                var studentAnswers = parseStudentAnswers(presenter.printableState);
                var correctAnswerList = correctAnswers.map(answer =&gt; answer.value);
                if (showAnswers) {
                    for (var i = 0; i &lt; studentAnswers.length; i++) {
                        for (var j = 0; j &lt; correctAnswers.length; j++) {
                            var index = correctAnswerList.indexOf(studentAnswers[i].value)
                            if (index &gt; -1) {
                                studentAnswers[i].isCorrect = true;
                                correctAnswerList.splice(index, 1);
                                break;
                            }
                        }
                        answerHTML = prepareCheckedAnswerToPrint(answerHTML, i, ...studentAnswers);
                    }
                } else {
                    answerHTML = prepareBasicPrint(answerHTML, null, ...studentAnswers);
                }
            } else {
                if (showAnswers) {
                    answerHTML = prepareBasicPrint(answerHTML, $wrapper, ...correctAnswers);
                }
            }
            $wrapper.html(textParser.parseAltTexts(answerHTML));
        }

        $view.append($wrapper);

        return $view[0].outerHTML;
    };

    function getCorrectAnswers() {
        var contextDict = {};
        var correctAnswers = []

        for (var i = 0; i &lt; presenter.configuration.itemsAnswersID.length; i++) {
            var splitItemAnswerID = presenter.configuration.itemsAnswersID[i].split("-");
            if (splitItemAnswerID.length === 2 &amp;&amp; !isNaN(splitItemAnswerID[1])) {
                var answerAddonID = splitItemAnswerID[0];
                var answerItemIndex = splitItemAnswerID[1] - 1;

                if (!(answerAddonID in contextDict)) {
                    contextDict[answerAddonID] = printableController.getPrintableContext(answerAddonID);
                }
                var hasDictItems = contextDict[answerAddonID] &amp;&amp; contextDict[answerAddonID].items;
                var isIndexCorrect = answerItemIndex &gt;= 0 &amp;&amp; answerItemIndex &lt; contextDict[answerAddonID].items.length;

                if (hasDictItems &amp;&amp; isIndexCorrect) {
                    var answer = {
                        value: contextDict[answerAddonID].items[answerItemIndex],
                        addonID: answerAddonID,
                        index: answerItemIndex
                    };
                    correctAnswers.push(answer);
                }
            }
        }

        return correctAnswers;
    }

    function getSeparatorBasedOnOrientation() {
        if (presenter.configuration.orientation === presenter.ORIENTATIONS.HORIZONTAL) {
            return ", ";
        } else {
            return "&lt;/br&gt;";
        }
    }

    function prepareBasicPrint (answerHTML, $wrapper=null ,...answers) {
        var separator = getSeparatorBasedOnOrientation()
        for (var i = 0; i &lt; answers.length; i++) {
            answerHTML = answerHTML + answers[i].value;
            if (i &lt; answers.length - 1) {
                answerHTML = answerHTML + separator;
            }
        }
        if ($wrapper !== null) {
            $wrapper.css("background-color", "#eeeeee");
        }
        return answerHTML;
    }

    function parseStudentAnswers (state) {
        var studentAnswers = [];
        for (var i = 0; i &lt; state.placeholders.length; i++){
            var splitItemAnswerID = state.placeholders[i].item.split("-");
            if (splitItemAnswerID.length === 2 &amp;&amp; !isNaN(splitItemAnswerID[1])){
                var answerAddonID = splitItemAnswerID[0];
                var answerItemIndex = splitItemAnswerID[1] - 1;
                var answer = {
                    value : state.placeholders[i].value,
                    addonID : answerAddonID,
                    index : answerItemIndex,
                    isCorrect : false
                };
                studentAnswers.push(answer);
            }
        }
        return studentAnswers;
    }

    function prepareCheckedAnswerToPrint (answerHTML, i, ...answers){
        element = document.createElement("span");
        element.classList.add("answerSpan");
        if (answers[i].isCorrect) {
            element.classList.add("correctAnswerSpan");
        } else {
            element.classList.add("incorrectAnswerSpan");
        }
        var separator = getSeparatorBasedOnOrientation()
        element.innerHTML = answers[i].value;
        answers[i].value = element.outerHTML;

        answerHTML = answerHTML + answers[i].value;
        if(i &lt; answers.length - 1){
            answerHTML = answerHTML + separator;
        }
        return answerHTML;
    }

    presenter.getActivitiesCount = function () {
        if (presenter.itemCounterMode) {
            return presenter.configuration.repetitions;
        } else {
            return presenter.configuration.itemsAnswersID.length;
        }
    }

    presenter.gradualShowAnswers = function (item) {
        presenter.showAnswer(item);
    }

    presenter.gradualHideAnswers = function () {
        presenter.removePlaceholders();
        presenter.hideAnswers();
        presenter.isGradualShowAnswersActive = false;
    }

    presenter.removePlaceholders = function () {
        presenter.$view.find('.placeholder').remove();
    }

    function setUpResetOnce() {
         presenter.resetOnce = (function() {
            let didReset = false;
            return function() {
                if(!didReset) {
                    didReset = true;
                    if(presenter.isShowAnswersActive){
                        presenter.hideAnswers();
                    }

                    presenter.$view.find('.placeholder').remove();
                    presenter.setWorkMode();
                    presenter.clearSelected();
                    presenter.keyboardControllerObject.setElements(presenter.getElementsForKeyboardNavigation());
                    presenter.elementCounter = 0;
                }
            };
        })();
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="text_identification" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" isLocalized="true" name="Text" nameLabel="text_identification_property_text" type="html"/>
		<property name="SelectionCorrect" nameLabel="text_identification_property_selection_correct" type="boolean"/>
		<property name="onSelected" nameLabel="text_identification_property_on_selected" type="event"/>
		<property name="onDeselected" nameLabel="text_identification_property_on_deselected" type="event"/>
        <property displayName="Block wrong answers" name="blockWrongAnswers" nameLabel="text_identification_property_block_wrong_answers" type="boolean"/>
		<property displayName="Send event on select/deselect" name="shouldSendEventsOnCommands" nameLabel="text_identification_property_send_event_on_command" type="boolean"/>
		<property displayName="Enable scrolling on mobile" name="enableScroll" nameLabel="text_identification_property_enable_scroll" type="boolean"/>
		<property name="Speech texts" nameLabel="Text_identification_property_speech_texts" type="staticlist">
			<property displayName="selected" name="Selected" type="staticrow">
                <property name="selected" nameLabel="Text_identification_property_selected" type="string"/>
            </property>
			<property displayName="deselected" name="Deselected" type="staticrow">
                <property name="deselected" nameLabel="Text_identification_property_deselected" type="string"/>
            </property>

			<property displayName="correct" name="Correct" type="staticrow">
                <property name="correct" nameLabel="Text_identification_property_correct" type="string"/>
            </property>
			<property displayName="incorrect" name="Incorrect" type="staticrow">
                <property name="incorrect" nameLabel="Text_identification_property_incorrect" type="string"/>
            </property>

		</property>
		<property displayName="Lang attribute" name="langAttribute" nameLabel="Text_identification_property_lang_attribute" type="string"/>
		<property name="printable" nameLabel="printable_name_label" type="{No, Don't randomize, Randomize}"/>
		<property name="isSplitInPrintBlocked" nameLabel="printable_block_split_label" type="boolean"/>
	</model>
<css>.text-identification-content {
    text-align: center;
    position: absolute;
}

.text-identification-element-selected,
.text-identification-element-correct,
.text-identification-element-incorrect,
.text-identification-element-mouse-hover,
.text-identification-element {
    border: 0 solid black;
    padding: 0;
    margin: 0;
}

.text-identification-container {
    cursor: pointer;
}

.text-identification-background-image {
    border: 0 solid white;
    padding: 0;
    margin: 0;
    background-size: 100%;
    background-position: 0 0;
}

.printable_addon_Text_Identification-wrapper {
    display: table-cell;
    vertical-align: middle;
    padding: 5px;
}

.printable_addon_Text_Identification-correct, .printable_addon_Text_Identification-selected {
    border: 3px solid #999;
}

.printable_addon_Text_Identification-show-answers.printable_addon_Text_Identification-correct {
    border: 3px dotted #999;
}

.printable_addon_Text_Identification-correct-answer, .printable_addon_Text_Identification-incorrect-answer {
    display: table-cell;
    vertical-align: middle;
}

.printable_addon_Text_Identification-correct-answer::after {
    content: " \2714";
}

.printable_addon_Text_Identification-incorrect-answer::after {
    content: " \2716";
}
</css><view/><preview/><presenter>function Addontext_identification_create() {

    function getTextVoiceObject (text, lang) { return {text: text, lang: lang}; }

    var presenter = function() {};

    var viewContainer;
    var isHoverEnabled = true;
    
    presenter.playerController = null;
    presenter.eventBus = null;
    presenter.lastEvent = null;
    presenter.isDisabled = false;
    presenter.keyboardControllerObject = null;
    presenter.printableState = null;
    presenter.printableStateMode = null;
    presenter.GSAcounter = 0;
    
    var CSS_CLASSES = {
        ELEMENT : "text-identification-element",
        SELECTED : "text-identification-element-selected",
        CORRECT : "text-identification-element-correct",
        INCORRECT : "text-identification-element-incorrect",
        EMPTY : 'text-identification-element-empty',
        MOUSE_HOVER_SELECTED : "text-identification-element-mouse-hover-selected",
        MOUSE_HOVER : "text-identification-element-mouse-hover",
        SHOW_ANSWERS : "text-identification-element-show-answers",
        PRINTABLE: "printable_addon_Text_Identification",
        PRINTABLE_WRAPPER: "printable_addon_Text_Identification-wrapper",
        PRINTABLE_SELECTED: "printable_addon_Text_Identification-selected",
        PRINTABLE_SHOW_ANSWERS: "printable_addon_Text_Identification-show-answers",
        PRINTABLE_SHOW_USER_ANSWERS: "printable_addon_Text_Identification-show-user-answers",
        PRINTABLE_CORRECT : "printable_addon_Text_Identification-correct",
        PRINTABLE_CORRECT_ANSWER: "printable_addon_Text_Identification-correct-answer",
        PRINTABLE_INCORRECT_ANSWER: "printable_addon_Text_Identification-incorrect-answer"
    };

    function CSS_CLASSESToString() {
        return CSS_CLASSES.ELEMENT + " " + CSS_CLASSES.SELECTED + " " + CSS_CLASSES.CORRECT + " " +
            CSS_CLASSES.INCORRECT + " " + CSS_CLASSES.EMPTY + " " + CSS_CLASSES.MOUSE_HOVER + " " + CSS_CLASSES.MOUSE_HOVER_SELECTED+ " " + CSS_CLASSES.SHOW_ANSWERS;
    }

    presenter.executeUserEventCode = function () {
        if (presenter.playerController == null) return;

        if (!presenter.isSelected()) {
            if (presenter.configuration.onDeselected) {
                presenter.playerController.getCommands().executeEventCode(presenter.configuration.onDeselected);
            }
        } else {
            if (presenter.configuration.onSelected) {
                presenter.playerController.getCommands().executeEventCode(presenter.configuration.onSelected);
            }
        }
    };

    presenter.triggerSelectionChangeEvent = function() {
        if (presenter.playerController == null) return;

        presenter.playerController.getEventBus().sendEvent('ValueChanged', this.createEventData());
    };

    presenter.clickHandler = function (e) {
        if(presenter.isDisabled){
            return;
        }
        if (e) {
            e.stopPropagation();
        }
        if (presenter.configuration.isErrorCheckMode) return;
        presenter.configuration.isSelected = !presenter.configuration.isSelected;
        presenter.applySelectionStyle(presenter.isSelected(), CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
        presenter.executeUserEventCode();
        presenter.triggerSelectionChangeEvent();
        if (presenter.isAllOK()) sendAllOKEvent();

        var score = presenter.configuration.shouldBeSelected ? 1 : 0;
        if(score == 0 &amp;&amp; presenter.configuration.blockWrongAnswers) {
            presenter.configuration.isSelected = !presenter.configuration.isSelected;
            presenter.applySelectionStyle(presenter.isSelected(), CSS_CLASSES.MOUSE_HOVER_SELECTED, CSS_CLASSES.ELEMENT);
        }
    };

    function handleMouseActions() {
        var $element = viewContainer.find('div.text-identification-container');
        if (!MobileUtils.isMobileUserAgent(navigator.userAgent)) {
            $element.hover(
                function () {
                    if (!presenter.configuration.isErrorCheckMode &amp;&amp; isHoverEnabled) {
                        $(this).removeClass(CSS_CLASSESToString());
                        $(this).addClass(presenter.isSelected() ? CSS_CLASSES.MOUSE_HOVER_SELECTED : CSS_CLASSES.MOUSE_HOVER);
                    }
                },
                function () {
                    if (!presenter.configuration.isErrorCheckMode &amp;&amp; isHoverEnabled) {
                        $(this).removeClass(CSS_CLASSESToString());
                        $(this).addClass(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);
                    }
                }
            );
        }

        if (presenter.configuration.enableScroll) {
            var posDiff = 0;
            var lastScreenPos = {X: 0, Y: 0};

            $element.on('touchstart', function (e) {
                e.stopPropagation();
                posDiff = 0;
                var temp = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];
                lastScreenPos.X = temp.screenX;
                lastScreenPos.Y = temp.screenY;
            });

            $element.on('touchend', function (e) {
                e.stopPropagation();
                if (posDiff &lt; 15) {
                    presenter.clickHandler(e);
                }
            });

            $element.on('touchmove', function (e) {
                e.stopPropagation();
                var temp = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0] || e.originalEvent.targetTouches[0];
                posDiff += Math.abs(lastScreenPos.X - temp.screenX) + Math.abs(lastScreenPos.Y - temp.screenY);
                lastScreenPos.X = temp.screenX;
                lastScreenPos.Y = temp.screenY;
            });

        } else {
            $element.on('touchstart', function (e) {
                e.preventDefault();

                presenter.lastEvent = e;
            });

            $element.on('touchend', function (e) {
                e.preventDefault();
                if (presenter.lastEvent.type != e.type) {
                    presenter.clickHandler(e);
                }
            });
        }

        if (!MobileUtils.isMobileUserAgent(navigator.userAgent)){
            $element.click(presenter.clickHandler);
        }
    }

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradeShouldSendEventsOnCommands(model);
        upgradedModel = upgradeModelEnableScrollProperty(upgradedModel);
        upgradedModel = presenter.upgradeTTS(upgradedModel);
        return upgradedModel;
    };

    function upgradeModelEnableScrollProperty(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if(!upgradedModel['enableScroll']){
            upgradedModel['enableScroll'] = false;
        }

        return upgradedModel;
    }

    presenter.upgradeShouldSendEventsOnCommands = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (model.shouldSendEventsOnCommands === undefined) {
            upgradedModel["shouldSendEventsOnCommands"] = "false";
        }

        return upgradedModel;
    };

    presenter.upgradeTTS = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        if (model['langAttribute'] === undefined) {
            upgradedModel['langAttribute'] = '';
        }

        return upgradedModel;
    };

    presenter.getSpeechTexts = function (model) {
       var speechTexts = model['Speech texts'];
       presenter.selectedSpeechText = 'Selected';
       presenter.deselectedSpeechText = 'Deselected';
       presenter.correctSpeechText = "Correct";
       presenter.incorrectSpeechText = "Incorrect";

        if (speechTexts !== undefined &amp;&amp; speechTexts !== '') {
            if (speechTexts['Selected']['selected'] !== '' &amp;&amp; speechTexts['Selected']['selected'] !== undefined) {
                presenter.selectedSpeechText = speechTexts['Selected']['selected'];
            }

            if (speechTexts['Deselected']['deselected'] !== '' &amp;&amp; speechTexts['Deselected']['deselected'] !== undefined) {
                presenter.deselectedSpeechText = speechTexts['Deselected']['deselected'];
            }

            if (!ModelValidationUtils.isArrayElementEmpty(speechTexts['Correct']['correct'])) {
                presenter.correctSpeechText = speechTexts['Correct']['correct'];
            }

            if (!ModelValidationUtils.isArrayElementEmpty(speechTexts['Incorrect']['incorrect'])) {
                presenter.incorrectSpeechText = speechTexts['Incorrect']['incorrect'];
            }
        }
    };

    presenter.validateModel = function (model) {
        return {
            addonID: model.ID,
            onSelected: model.onSelected,
            onDeselected: model.onDeselected,
            shouldBeSelected: ModelValidationUtils.validateBoolean(model.SelectionCorrect),
            isSelected: false,
            isErrorCheckMode: false,
            blockWrongAnswers: ModelValidationUtils.validateBoolean(model.blockWrongAnswers),
            shouldSendEventsOnCommands: ModelValidationUtils.validateBoolean(model.shouldSendEventsOnCommands),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]),
            enableScroll: ModelValidationUtils.validateBoolean(model['enableScroll']),
        };
    };

    presenter.centerElements = function ($text, $container) {
        $.when(presenter.mathJaxProcessEnded).then(function () {
            var contentWidth = parseInt($text.css('width'), 10),
                contentHeight = parseInt($text.css('height'), 10),
                containerWidth = parseInt(viewContainer.css('width'), 10),
                containerHeight = parseInt(viewContainer.css('height'), 10);

            $text.css({
                left: Math.round((containerWidth - contentWidth) / 2) + 'px',
                top: Math.round((containerHeight - contentHeight) / 2) + 'px'
            });

            $container.css({
                width: containerWidth + 'px',
                height: containerHeight + 'px'
            });
        });
    };

    function presenterLogic(view, model, isPreview) {
        presenter.registerMathJaxListener(isPreview);

        viewContainer = $(view);
        presenter.$view = $(view);
        presenter.currentPageId = presenter.$view.parent('.ic_page').attr('id');
        var textSrc = model.Text ? model.Text : "";
        presenter.moduleID = model.ID;
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);

        presenter.getSpeechTexts(model);
        presenter.langTag = model['langAttribute'];

        presenter.isVisible = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.isVisibleByDefault = ModelValidationUtils.validateBoolean(model["Is Visible"]);
        presenter.setVisibility(presenter.isVisible || isPreview);

        var container = $('&lt;div class="text-identification-container"&gt;&lt;/div&gt;');
        container.addClass(presenter.isSelected() ? CSS_CLASSES.SELECTED : CSS_CLASSES.ELEMENT);

        presenter.setTabindex(container,presenter.configuration.isTabindexEnabled);

        var text = $('&lt;div class="text-identification-content"&gt;&lt;/div&gt;');
        text.html(presenter.textParser.parse(textSrc));
        container.append(text);

        viewContainer.append(container);
        presenter.centerElements(text, container);

        if (!isPreview) handleMouseActions();
        presenter.buildKeyboardController();
    }

    presenter.setVisibility = function (isVisible) {
        $(presenter.$view).css('visibility', isVisible ? 'visible' : 'hidden');
    };

    presenter.hide = function () {
        presenter.setVisibility(false);
        presenter.isVisible = false;
    };

    presenter.show = function () {
        presenter.setVisibility(true);
        presenter.isVisible = true;
    };

    presenter.setPlayerController = function (controller) {
        presenter.playerController = controller;
        presenter.eventBus = controller.getEventBus();
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    };

    presenter.setPreviewTextParser = function (getTextParser) {
        presenter.textParser = new TextParserProxy(getTextParser());
    };

    /**
     * @param controller (PrintableController)
     */
    presenter.setPrintableController = function (controller) {
        presenter.textParser = new TextParserProxy(controller.getTextParser());
    }

    presenter.applySelectionStyle = function (selected, selectedClass, unselectedClass) {
        var element = viewContainer.find('div:first')[0];

        $(element).removeClass(CSS_CLASSESToString());
        $(element).addClass(selected ? selectedClass : unselectedClass);
    };

    presenter.select = function () {
        var wasSelected = presenter.configuration.isSelected;

        presenter.configuration.isSelected = true;
        presenter.executeUserEventCode();
        presenter.applySelectionStyle(true, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        if (!wasSelected &amp;&amp; presenter.configuration.shouldSendEventsOnCommands) {
            this.triggerSelectionChangeEvent();
        }

    };

    presenter.deselect = function () {
        var wasSelected = presenter.configuration.isSelected;

        presenter.configuration.isSelected = false;
        presenter.executeUserEventCode();
        presenter.applySelectionStyle(false, CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        if (wasSelected &amp;&amp; presenter.configuration.shouldSendEventsOnCommands) {
            this.triggerSelectionChangeEvent();
        }
    };

    presenter.isSelected = function () {
        return presenter.configuration.isSelected;
    };

    presenter.markAsCorrect = function() {
        isHoverEnabled = false;
        presenter.configuration.isSelected = true;
        presenter.applySelectionStyle(true, CSS_CLASSES.CORRECT, CSS_CLASSES.ELEMENT);
    };

    presenter.markAsWrong = function() {
        isHoverEnabled = false;
        presenter.configuration.isSelected = true;
        presenter.applySelectionStyle(true, CSS_CLASSES.INCORRECT, CSS_CLASSES.ELEMENT);
    };

    presenter.markAsEmpty = function() {
        isHoverEnabled = false;
        presenter.applySelectionStyle(true, CSS_CLASSES.EMPTY, CSS_CLASSES.ELEMENT);
    };

    function sendAllOKEvent() {
        var eventData = {
            'source': presenter.moduleID,
            'item': 'all',
            'value': '',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    }

    presenter.executeCommand = function(name, params) {
        if (presenter.configuration.isErrorCheckMode) return;

        var commands = {
            'select': presenter.select,
            'deselect': presenter.deselect,
            'isSelected': presenter.isSelected,
            'markAsCorrect': presenter.markAsCorrect,
            'markAsWrong': presenter.markAsWrong,
            'markAsEmpty': presenter.markAsEmpty,
            'isAllOK': presenter.isAllOK,
            'show': presenter.show,
            'hide': presenter.hide,
            'disable': presenter.disable,
            'enable': presenter.enable
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.disable = function() {
        presenter.isDisabled = true;
        var $element = viewContainer.find('div.text-identification-container');
        $element.addClass("text-identification-element-disabled");
    };

    presenter.enable = function() {
        presenter.isDisabled = false;
        var $element = viewContainer.find('div.text-identification-container');
        $element.removeClass("text-identification-element-disabled");
    };

    presenter.registerMathJaxListener = function (isPreview) {
        var mathJaxDeferred = new jQuery.Deferred();
        presenter.mathJaxProcessEndedDeferred = mathJaxDeferred;
        presenter.mathJaxProcessEnded = mathJaxDeferred.promise();

        MathJax.Hub.Register.MessageHook("End Process", function (message) {
            // We're listening for "End Process" that was fired for ic_page into which addon was inserted.
            // This way we're not reacting on events from other page in Book View.
            if (isPreview || ($(message[1]).hasClass('ic_page') &amp;&amp; $(message[1]).is('#' + presenter.currentPageId))) {
                presenter.mathJaxProcessEndedDeferred.resolve();
            }
        });
    };

    presenter.createPreview = function(view, model) {
        presenterLogic(view, model, true);
    };

    presenter.run = function(view, model){
        presenterLogic(view, model, false);

        var events = ['ShowAnswers', 'HideAnswers', 'GradualShowAnswers', 'GradualHideAnswers'];
        for (var i = 0; i &lt; events.length; i++) {
            presenter.eventBus.addEventListener(events[i], this);
        }
    };

    presenter.reset = function() {
        presenter.configuration.isSelected = false;
        presenter.configuration.isErrorCheckMode = false;
        isHoverEnabled = true;
        presenter.applySelectionStyle(presenter.isSelected(), CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
        presenter.setVisibility(presenter.isVisibleByDefault);
        presenter.isVisible = presenter.isVisibleByDefault;
    };

    presenter.setWorkMode = function() {
        presenter.configuration.isErrorCheckMode = false;

        presenter.applySelectionStyle(presenter.isSelected(), CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);
    };

    presenter.setShowErrorsMode = function() {
        if (presenter.isShowAnswersActive) {
            presenter.hideAnswers();
        }

        presenter.configuration.isErrorCheckMode = true;

        if (presenter.isSelected()) {
            presenter.applySelectionStyle(presenter.isSelected() === presenter.configuration.shouldBeSelected, CSS_CLASSES.CORRECT, CSS_CLASSES.INCORRECT);
        } else {
            presenter.applySelectionStyle(true, CSS_CLASSES.EMPTY, CSS_CLASSES.ELEMENT)
        }
    };

    presenter.isAllOK = function () {
        return presenter.getMaxScore() === presenter.getScore() &amp;&amp; presenter.getErrorCount() === 0;
    };

    presenter.getErrorCount = function() {
        return !presenter.configuration.shouldBeSelected &amp;&amp; presenter.isSelected() ? 1 : 0;
    };

    presenter.getMaxScore = function() {
        return presenter.configuration.shouldBeSelected ? 1 : 0;
    };

    presenter.getScore = function() {
        return presenter.configuration.shouldBeSelected &amp;&amp; presenter.isSelected() ? 1 : 0;
    };

    presenter.getState = function() {
        //return presenter.isSelected() ? 'True' : 'False';

        return JSON.stringify({
            isSelected: presenter.isSelected() ? 'True' : 'False',
            isVisible: presenter.isVisible,
            isDisabled: presenter.isDisabled
        });
    };

    presenter.setState = function(state) {
        var serializeIsSelected, parsedState;
        if (state.indexOf("}") &gt; -1 &amp;&amp; state.indexOf("{") &gt; -1){
            parsedState = JSON.parse(state);
            serializeIsSelected = parsedState.isSelected;
        }else{
            serializeIsSelected = state;
            parsedState = undefined;
        }

        presenter.configuration.isSelected = serializeIsSelected.toString() === "True";

        presenter.applySelectionStyle(presenter.isSelected(), CSS_CLASSES.SELECTED, CSS_CLASSES.ELEMENT);

        if(parsedState){
            if(parsedState.isVisible != undefined){
                presenter.setVisibility(parsedState.isVisible);
                presenter.isVisible = parsedState.isVisible;
            }

            if(parsedState.isDisabled != undefined){
                presenter.isDisabled = parsedState.isDisabled;
            }
        }
    };

    presenter.createEventData = function() {
        return {
            'source' : presenter.configuration.addonID,
            'item' : '1',
            'value' : presenter.isSelected() ? '1' : '0',
            'score' : presenter.configuration.shouldBeSelected ? '1' : '0'
        }
    };

    presenter.getActivitiesCount = function () {
        if(presenter.configuration.shouldBeSelected) 
            return 1;
        return 0;
    }

    presenter.onEventReceived = function (eventName, data) {
        if (eventName == "ShowAnswers") {
            presenter.showAnswers();
        } else if (eventName == "HideAnswers") {
            presenter.hideAnswers();
        } else if (eventName === "GradualShowAnswers") {
            presenter.GSAcounter++;
            if (!presenter.isGradualShowAnswersActive) {
                presenter.isGradualShowAnswersActive = true;
            }
            if(presenter.GSAcounter === 1) presenter.hideStudentAnswersForGSA();
            if (data.moduleID === presenter.configuration.addonID) {
                presenter.showAnswers();
            }
        } else if (eventName === "GradualHideAnswers") {
            presenter.hideAnswers();
            presenter.isGradualShowAnswersActive = false;
        }
    };

    function applySelectionStyleShowAnswers (style) {
        var element = presenter.$view.find('div:first')[0];
        $(element).addClass(style);
    }

    function applySelectionStyleHideAnswers (style) {
        var element = presenter.$view.find('div:first')[0];

        $(element).removeClass(style);
        $(element).removeClass(CSS_CLASSES.EMPTY).addClass(CSS_CLASSES.ELEMENT);
    }

    presenter.hideStudentAnswersForGSA = function () {
        presenter.isShowAnswersActive = true;
        presenter.configuration.isErrorCheckMode = true;
        
        presenter.$view.find('.text-identification-element-selected').removeClass(CSS_CLASSES.SELECTED).addClass("text-identification-element was-selected");
    }

    presenter.showAnswers = function () {
        presenter.isShowAnswersActive = true;
        presenter.configuration.isErrorCheckMode = true;
        
        presenter.$view.find('.text-identification-element-incorrect').removeClass(CSS_CLASSES.INCORRECT).addClass("text-identification-element was-selected");
        presenter.$view.find('.text-identification-element-correct').removeClass(CSS_CLASSES.CORRECT).addClass("text-identification-element was-selected");

        if(presenter.configuration.shouldBeSelected){
            applySelectionStyleShowAnswers(CSS_CLASSES.SHOW_ANSWERS);
        }else{
            presenter.$view.find('.text-identification-element-selected').removeClass(CSS_CLASSES.SELECTED).addClass("text-identification-element was-selected");
        }
    };

    presenter.hideAnswers = function () {
        if (!presenter.isShowAnswersActive) {
            return;
        }

        presenter.configuration.isErrorCheckMode = false;

        applySelectionStyleHideAnswers(CSS_CLASSES.SHOW_ANSWERS);

        var elementWasSelected = presenter.$view.find('.was-selected');
        $(elementWasSelected).addClass(CSS_CLASSES.SELECTED).removeClass("was-selected");

        presenter.isShowAnswersActive = false;
        presenter.GSAcounter = 0;
    };

    function TextIdentificationKeyboardController (elements, columnsCount) {
        KeyboardController.call(this, elements, columnsCount);
    }

    TextIdentificationKeyboardController.prototype = Object.create(window.KeyboardController.prototype);
    TextIdentificationKeyboardController.prototype.constructor = TextIdentificationKeyboardController;

    TextIdentificationKeyboardController.prototype.enter = function (event) {
        KeyboardController.prototype.enter.call(this, event);

        presenter.readElement();
    };

    TextIdentificationKeyboardController.prototype.getTarget = function (element, willBeClicked) {
        return $(element);
    };

    TextIdentificationKeyboardController.prototype.select = function (event) {
        event.preventDefault();
        presenter.clickHandler(event);

        if (!presenter.isShowAnswersActive &amp;&amp; !presenter.configuration.isErrorCheckMode) {
            presenter.readSelected();
        }
    };


    presenter.keyboardController = function(keycode, isShiftKeyDown, event) {
        this.keyboardControllerObject.handle(keycode, isShiftKeyDown, event);
    };

    presenter.buildKeyboardController = function () {
        var element = $(presenter.$view).find('.text-identification-container');

        presenter.keyboardControllerObject = new TextIdentificationKeyboardController(element, 1);
    };

    presenter.readSelected = function () {
        var text, voiceObject;

        if (presenter.isShowAnswersActive) {
            text = presenter.configuration.shouldBeSelected ? presenter.selectedSpeechText : presenter.deselectedSpeechText;
        } else {
            text = presenter.configuration.isSelected ? presenter.selectedSpeechText : presenter.deselectedSpeechText;
        }

        voiceObject = getTextVoiceObject(text);

        speak([voiceObject]);
    };

    presenter.readElement = function () {
        var voiceObjects = window.TTSUtils.getTextVoiceArrayFromElement(presenter.$view.find('.text-identification-content'), presenter.langTag);

        var selectedTextObject = getTextVoiceObject(presenter.selectedSpeechText);

        if (!presenter.isShowAnswersActive &amp;&amp; !presenter.configuration.isErrorCheckMode &amp;&amp; presenter.configuration.isSelected) {
            voiceObjects.push(selectedTextObject);
        } else if (presenter.isShowAnswersActive &amp;&amp; presenter.configuration.isErrorCheckMode &amp;&amp; presenter.configuration.shouldBeSelected) {
            voiceObjects.push(selectedTextObject);
        }

        // correctness should be read only when check mode is active and addon is selected
        if (!presenter.isShowAnswersActive &amp;&amp; presenter.configuration.isErrorCheckMode &amp;&amp;  presenter.configuration.isSelected) {
            voiceObjects.push(selectedTextObject);

            var isAnswerCorrect = presenter.configuration.isSelected === presenter.configuration.shouldBeSelected;
            var isAnswerCorrectText = isAnswerCorrect ? presenter.correctSpeechText : presenter.incorrectSpeechText;
            voiceObjects.push(getTextVoiceObject(isAnswerCorrectText));
        }

        speak(voiceObjects, presenter.langTag);
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    presenter.setTabindex = function (element, isTabindexEnabled) {
        var tabindexValue = isTabindexEnabled ? "0" : "-1";
        element.attr("tabindex", tabindexValue);
    };

    function speak (voiceObjects) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts) {
            tts.speak(voiceObjects);
        }
    }

    presenter.PRINTABLE_STATE_MODE = {
        EMPTY: 0,
        SHOW_ANSWERS: 1,
        SHOW_USER_ANSWERS: 2,
        CHECK_ANSWERS: 3
    };

    presenter.setPrintableState = function(state) {
        presenter.printableState = null;

        if (state === null || ModelValidationUtils.isStringEmpty(state)) {
            return;
        }

        var serializedIsSelected, parsedState;
        if (state.indexOf("}") &gt; -1 &amp;&amp; state.indexOf("{") &gt; -1) {
            parsedState = JSON.parse(state);
            serializedIsSelected = parsedState.isSelected;
        } else {
            parsedState = undefined;
            serializedIsSelected = state;
        }

        presenter.printableState = {};
        presenter.printableState.isSelected = serializedIsSelected.toString() === "True";
        if (parsedState) {
            if(parsedState.isVisible !== undefined)
                presenter.printableState.isVisible = parsedState.isVisible;

            if(parsedState.isDisabled !== undefined)
                presenter.printableState.isDisabled = parsedState.isDisabled;
        }
    };

    presenter.getPrintableHTML = function (model, showAnswers) {
        const text = setUpLogicForPrintable(model);
        setUpPresenterViewForPrintable();
        chosePrintableStateMode(showAnswers);
        addHTMLRepresentationAccordingToPrintableStateMode(text);
        presenter.printableStateMode = null;
        return presenter.$view[0].outerHTML;
    };

    function setUpLogicForPrintable(model) {
        presenter.moduleID = model.ID;
        model = presenter.upgradeModel(model);
        presenter.configuration = presenter.validateModel(model);
        return presenter.textParser.parse(model.Text);
    }

    function setUpPresenterViewForPrintable() {
        presenter.$view = $('&lt;div&gt;&lt;/div&gt;');
        presenter.$view.addClass(CSS_CLASSES.PRINTABLE);
    }

    function chosePrintableStateMode(showAnswers) {
        if (presenter.printableState) {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS;
        } else {
            if (showAnswers)
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS;
            else
                presenter.printableStateMode = presenter.PRINTABLE_STATE_MODE.EMPTY;
        }
    }

    function addHTMLRepresentationAccordingToPrintableStateMode(text) {
        switch (presenter.printableStateMode) {
            case presenter.PRINTABLE_STATE_MODE.CHECK_ANSWERS:
                addHTMLRepresentationForCheckAnswersPrintableStateMode(text);
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_USER_ANSWERS:
                addHTMLRepresentationForShowUserAnswersPrintableStateMode(text);
                break;
            case presenter.PRINTABLE_STATE_MODE.SHOW_ANSWERS:
                addHTMLRepresentationForShowAnswersPrintableStateMode(text);
                break;
            case presenter.PRINTABLE_STATE_MODE.EMPTY:
                addHTMLRepresentationForEmptyPrintableStateMode(text);
                break;
        }
    }

    function addHTMLRepresentationForCheckAnswersPrintableStateMode(text) {
        var $wrapper = createPrintableAddonWrapper(text);
        if (presenter.printableState.isSelected)
            $wrapper.addClass(CSS_CLASSES.PRINTABLE_SELECTED);
        presenter.$view.append($wrapper);
        addHTMLRepresentingCorrectnessOfAnswer();
    }

    function addHTMLRepresentingCorrectnessOfAnswer() {
        if (presenter.printableState.isSelected) {
            var $element = $('&lt;div&gt;&lt;/div&gt;');

            if (presenter.configuration.shouldBeSelected)
                $element.addClass(CSS_CLASSES.PRINTABLE_CORRECT_ANSWER)
            else
                $element.addClass(CSS_CLASSES.PRINTABLE_INCORRECT_ANSWER)

            presenter.$view.append($element);
        }
    }

    function addHTMLRepresentationForShowUserAnswersPrintableStateMode(text) {
        var $wrapper = createPrintableAddonWrapper(text);
        $wrapper.addClass(CSS_CLASSES.PRINTABLE_SHOW_USER_ANSWERS);
        if (presenter.printableState.isSelected)
            $wrapper.addClass(CSS_CLASSES.PRINTABLE_SELECTED);
        presenter.$view.append($wrapper);
    }

    function addHTMLRepresentationForShowAnswersPrintableStateMode(text) {
        var $wrapper = createPrintableAddonWrapper(text);
        $wrapper.addClass(CSS_CLASSES.PRINTABLE_SHOW_ANSWERS);
        if (presenter.configuration.shouldBeSelected)
            $wrapper.addClass(CSS_CLASSES.PRINTABLE_CORRECT);
        presenter.$view.append($wrapper);
    }

    function addHTMLRepresentationForEmptyPrintableStateMode(text) {
        var $wrapper = createPrintableAddonWrapper(text);
        presenter.$view.append($wrapper);
    }

    function createPrintableAddonWrapper(text) {
        var $wrapper = $('&lt;div&gt;&lt;/div&gt;');
        $wrapper.addClass(CSS_CLASSES.PRINTABLE_WRAPPER);
        var textSrc = text ? text : "";
        $wrapper.html(textSrc);
        return $wrapper;
    }

    return presenter;
}
</presenter></addon><addon xmlns="http://icplayer.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="video" xsi:schemaLocation="http://icplayer.com ../../tools/addon.xsd">
	<model>
		<property isDefault="true" name="Files" nameLabel="video_property_files" type="list">
			<property displayName="Ogv video" name="Ogg video" nameLabel="video_property_ogg_video" type="video"/>
			<property name="MP4 video" nameLabel="video_property_mp4_video" type="video"/>
			<property name="WebM video" nameLabel="video_property_webm_video" type="video"/>
			<property name="m3u8 video" nameLabel="video_property_m3u8_video" type="video"/>
			<property isLocalized="true" name="Subtitles" nameLabel="video_property_subtitles" type="text"/>
			<property name="Poster" nameLabel="video_property_poster" type="image"/>
			<property name="ID" nameLabel="video_property_id" type="string"/>
            <property name="Loop video" nameLabel="video_property_loop_video" type="boolean"/>
			<property name="AlternativeText" nameLabel="video_property_alternative_text" type="string"/>
			<property name="time_labels" nameLabel="video_property_time_labels" type="text"/>
			<property isLocalized="true" name="Audio Description" nameLabel="video_property_audiodescription" type="text"/>
		</property>
		<property isLocalized="true" name="Narration" nameLabel="video_property_narration" type="narration"/>
        <property name="Hide default controls" nameLabel="video_property_hide_default_controls" type="boolean"/>
        <property name="Hide subtitles" nameLabel="video_property_hide_subtitles" type="boolean"/>
		<property name="Show play button" nameLabel="video_property_show_play_button" type="boolean"/>
		<property name="speechTexts" nameLabel="feedback_property_speech_texts" type="staticlist">
            <property name="AudioDescriptionEnabled" nameLabel="video_property_description_enabled" type="staticrow">
                <property name="AudioDescriptionEnabled" nameLabel="video_property_description_enabled" type="string"/>
            </property>
            <property name="AudioDescriptionDisabled" nameLabel="video_property_description_disabled" type="staticrow">
                <property name="AudioDescriptionDisabled" nameLabel="video_property_description_disabled" type="string"/>
            </property>
        </property>
        <property name="offlineMessage" nameLabel="video_property_offline_message" type="text"/>
		<property name="enableVideoSpeedController" nameLabel="enable_video_speed_controller" type="boolean"/>
		<property name="Base width" nameLabel="video_property_base_width" type="string"/>
		<property name="Base height" nameLabel="video_property_base_height" type="string"/>
		<property name="{library}" nameLabel="video_property_hls_library" type="module-script" value="/file/serve/5350926942339072"/>
	</model>
<css>.video-container {
    background-color: #000;
    position: relative;
}

.captions {
    position: absolute;
    color: #fff;
}

.poster-wrapper {
    display: block;
    position: absolute;
    top:0;
    left:0;
    width: 100%;
    height: 100%;
    background-color: white;
}

.poster-wrapper img {
    position : absolute;
    top:0;
    left:0;
    width: 100%;
    height: 100%;
}

.video-poster-play {
    position : absolute;
    background: url('resources/video-poster-play.svg') no-repeat center;
    text-align: center;
    width: 80px;
    height: 80px;
    top: calc(50% - 40px);
    left: calc(50% - 40px);

}

.video-container-mask {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0px;
    left: 0px;
    color: transparent;
    overflow: hidden;
}

.video-container .CustomControlsBar-wrapper {
    bottom : 0px;
    left : 0px;
    background-color: #EBEFF0;
    height: 40px;
    width: 100%;
    position: absolute;
}

.video-container .CustomControlsBar-wrapper-controls-controlsWrapper {
    margin-top: 3px;
    position: absolute;
    height: 37px;
    width: 100%;
}

.video-container-video {
    display: block;
    pointer-events: none;
    position: absolute;
}

.video-container .CustomControlsBar-wrapper-controls-play {
    height: 100%;
    width: 38px;
    background: url("resources/video-play-btn.png") no-repeat center;
    box-sizing: border-box;
    cursor: pointer;
    float: left;

}

.video-container .CustomControlsBar-wrapper-controls-pause {
    height: 100%;
    width: 38px;
    background: url("resources/video-pause-btn.png") no-repeat center;
    box-sizing: border-box;
    cursor: pointer;
    float: left;

}

.video-container .CustomControlsBar-wrapper-controls-stop {
    height: 100%;
    width: 40px;
    background: url("resources/video-stop-btn.png") no-repeat center center;
    cursor: pointer;
    float: left;

}

.video-container .CustomControlsBar-wrapper-controls-progressBarWrapper {
    position:absolute;
    width: 100%;
    height: 3px;
    float: left;
}

.video-container .CustomControlsBar-wrapper-controls-progressBarWrapper:hover {
    height: 6px;
}

.video-container .CustomControlsBar-wrapper-controls-progressBarWrapper-redProgressBar {
    background-color: red;
    height: 100%;
    position:absolute;
}

.video-container .CustomControlsBar-wrapper-controls-progressBarWrapper-grayProgressBar {
    background-color: dimgrey;
    width: 100%;
    height: 100%;
    position:absolute;
}

.video-container .CustomControlsBar-wrapper-controls-volume {
    height: 100%;
    width: 40px;
    background: url("resources/video-volume-btn.png") no-repeat center center;
    cursor: pointer;
    float: left;

}

.video-container .CustomControlsBar-wrapper-controls-volumeBarWrapper {
    height: 100%;
    float: left;
    cursor: pointer;
}

.video-container .CustomControlsBar-wrapper-controls-volumeBarWrapper-volumeBackground {
    width: 100px;
    height: 14px;
    border-bottom: solid 9px white;
    float: left;
    cursor: pointer;
    position: absolute;
}

.video-container .CustomControlsBar-wrapper-controls-volumeBarWrapper-volumeBackgroundSelected {
    width: 100px;
    height: 14px;
    border-bottom: solid 9px lightgrey;
    float: left;
    cursor: pointer;
    position: absolute;
}

.video-container .CustomControlsBar-wrapper-controls-fullscreen {
    float: right;
    width: 40px;
    height: 100%;
    background: url("resources/video-fullscreen-btn.png") no-repeat center center;
    cursor: pointer;
}

.video-container .CustomControlsBar-wrapper-controls-closeFullscreen {
    float: right;
    width: 40px;
    height: 100%;
    background: url("resources/video-close-fullscreen-btn.png") no-repeat center center;
    cursor: pointer;
}

.video-container .CustomControlsBar-wrapper-controls-timer {
    height: 40px;
    color: gray;
    font-size: 14px;
    padding-right: 10px;
    display: flex;
    align-items: center;
    float: right;

}

.CustomControlsBar-wrapper-controls-burgersContainer-time_labels {
    height: 100%;
    width: 40px;
    background: url("resources/video-drop-down.png") no-repeat center;
    cursor: pointer;
    float: left;
}

.CustomControlsBar-wrapper-controls-burgersContainer {
    float: right;
    height: 100%;
}

.CustomControlsBar-wrapper-controls-burgersContainer-container-time_labels {
    background-color: #f9f9f9;
    min-width: 100px;
    box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
    position: absolute;
    display: none;
    z-index: 1;
    white-space: nowrap;
    word-break: keep-all;
}

.CustomControlsBar-wrapper-controls-burgersContainer-time_labels-element {
    padding: 5px;
}

.CustomControlsBar-wrapper-controls-burgersContainer-time_labels-element:hover {
    background-color: #f1f1f1;
}

.CustomControlsBar-wrapper-controls-videoSpeedController .video-playback-rate{
    float: left;
    margin-top: 10px;
    height: 30px;
    width: 60px;
    line-height: 16px;
    padding-right: 10px;
    background-color: inherit;
    cursor: pointer;
}

.CustomControlsBar-wrapper-controls-videoSpeedController .video-playback-rate select {
    width: 100%;
    font-size: 14px;
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
    color: grey;
    border: 0;
    background-color: inherit;
    cursor: pointer;
}

.CustomControlsBar-wrapper-controls-videoSpeedController .video-playback-rate select option{
    cursor: pointer;
}

.video-container .video-js {
    height: 0;
}

.video-container .video-js :not(video) {
    display: none;
    visibility: hidden;
}

.video-container .video-js video {
    position: absolute;
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100%;
}
</css><view>&lt;div class="video-container"&gt;
    &lt;video class="video-container-video"&gt;
        Video playback is not supported on your browser.
    &lt;/video&gt;
    &lt;div class="captions-container"&gt;&lt;/div&gt;
    &lt;div class="video-container-mask"&gt;&lt;/div&gt;
    &lt;div class="poster-wrapper"&gt;
    &lt;/div&gt;
    &lt;div class="video-poster-play"&gt;&lt;/div&gt;
&lt;/div&gt;
</view><preview>&lt;div class="video-container"&gt;
    &lt;video preload="auto"&gt;
        Video playback is not supported on your browser.
    &lt;/video&gt;
&lt;/div&gt;
</preview><presenter>/**
 * @module VideoAddon
 * @class VideoAddon
 * @constructor
 */
function Addonvideo_create() {
    var presenter = function () {
    };

    var deferredSyncQueue = window.DecoratorUtils.DeferredSyncQueue(deferredQueueDecoratorChecker);
    var currentTime;

    var escapedSeparator = '&amp;&amp;separator&amp;&amp;';

    presenter.currentMovie = 0;
    presenter.videoContainer = null;
    presenter.$view = null;
    presenter.files = [];
    presenter.video = null;
    presenter.hlsPlayer = null;
    presenter.isHLSValue = null;
    presenter.controlBar = null;
    presenter.isVideoSpeedControllerAdded = false;
    presenter.isCurrentlyVisible = true;
    presenter.isVideoLoaded = false;
    presenter.metadadaLoaded = false;
    presenter.isPreview = false;
    presenter.captions = [];
    presenter.captionDivs = [];
    presenter.descriptions = [];
    presenter.descriptionsDivs = [];
    presenter.speechTexts = [];
    presenter.metadataQueue = [];
    presenter.areSubtitlesHidden = false;
    presenter.calledFullScreen = false;
    presenter.playTriggered = false;
    presenter.playerController = null;
    presenter.posterPlayButton = null;
    presenter.videoView = null;
    presenter.isAudioDescriptionEnabled = null;
    presenter.prevTime = -0.001;
    presenter.usedStop = false;
    presenter.stylesBeforeFullscreen = {
        changedStyles: false,
        style: null,
        moduleWidth: 0,
        moduleHeight: 0,
        actualTime: -1,
        className: ''
    };

    presenter.originalVideoSize = {
        width: 0,
        height: 0
    };

    presenter.captionsOffset = {
        left: 0,
        top: 0
    };

    presenter.captionsScale = {
        xScale: 1.0,
        yScale: 1.0
    };

    presenter.lastWidthAndHeightValues = {
        width: 0,
        height: 0
    };

    presenter.addedVideoURLS = {};

    presenter.configuration = {
        isValid: false,
        addonSize: {
            width: 0,
            height: 0
        },
        addonID: null,
        isVisibleByDefault: null,
        shouldHideSubtitles: null,
        defaultControls: null,
        files: [],
        height: 0,
        showPlayButton: false,
        offlineMessage: "",
        baseDimensions: {
            width: 0,
            height: 0
        }
    };

    presenter.lastSentCurrentTime = 0;

    function deferredQueueDecoratorChecker() {
        return presenter.isVideoLoaded;
    }

    presenter.metadataLoadedDecorator = function (fn) {
        return function () {
            if (presenter.metadadaLoaded) {
                return fn.apply(this, arguments);
            } else {
                presenter.pushToMetadataQueue(fn, arguments);
            }
        }
    };

    presenter.pushToMetadataQueue = function (fn, providedArguments) {
        presenter.metadataQueue.push({
            function: fn,
            arguments: providedArguments,
            self: this
        });
    };

    presenter.upgradeShowPlayButton = function (model) {
        if (!model['Show play button']) {
            model['Show play button'] = 'False';
        }

        return model;
    };

    presenter.upgradeTimeLabels = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy

        for (var i = 0; i &lt; model.Files.length; i++) {
            if (!upgradedModel.Files[i].time_labels) {
                upgradedModel.Files[i].time_labels = "";
            }
        }

        return upgradedModel;
    };

    presenter.upgradeHLS = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy

        for (var i = 0; i &lt; model.Files.length; i++) {
            if (!upgradedModel.Files[i]['m3u8 video']) {
                upgradedModel.Files[i]['m3u8 video'] = "";
            }
        }

        return upgradedModel;
    };

    presenter.upgradeModel = function (model) {
        var upgradedModel = presenter.upgradePoster(model);
        upgradedModel = presenter.upgradeTimeLabels(upgradedModel);
        upgradedModel = presenter.upgradeSpeechTexts(upgradedModel);
        upgradedModel = presenter.upgradeOfflineMessage(upgradedModel);
        upgradedModel = presenter.upgradeVideoSpeedController(upgradedModel);
        upgradedModel = presenter.upgradeShowPlayButton(upgradedModel);
        upgradedModel = presenter.upgradeBaseDimensions(upgradedModel);
        upgradedModel = presenter.removeShowVideoFromModel(upgradedModel);
        return presenter.upgradeHLS(upgradedModel);
    };

    presenter.upgradePoster = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        for (var i = 0; i &lt; model.Files.length; i++) {
            if (!upgradedModel.Files[i].Poster) {
                upgradedModel.Files[i].Poster = "";
            }
        }

        return upgradedModel;
    };

    presenter.upgradeSpeechTexts = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model); // Deep copy of model object

        for (var i = 0; i &lt; model.Files.length; i++) {
            if (!upgradedModel.Files[i]["Audio Description"]) {
                upgradedModel.Files[i]["Audio Description"] = "";
            }
        }

        if (!model.speechTexts) {
            upgradedModel.speechTexts = {
                AudioDescriptionEnabled: {AudioDescriptionEnabled: "Audio description enabled"},
                AudioDescriptionDisabled: {AudioDescriptionDisabled: "Audio description disabled"}
            }
        }

        return upgradedModel;
    };

    presenter.upgradeOfflineMessage = function (model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel["offlineMessage"]) {
            upgradedModel["offlineMessage"] = "This video is not available offline. Please connect to the Internet to watch it."
        }

        return upgradedModel;
    };

    presenter.upgradeVideoSpeedController = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('enableVideoSpeedController')) {
            upgradedModel['enableVideoSpeedController'] = 'False';
        }

        return upgradedModel;
    }

    presenter.upgradeBaseDimensions = function(model) {
        var upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (!upgradedModel.hasOwnProperty('Base width')) {
            upgradedModel['Base width'] = '';
        }

        if (!upgradedModel.hasOwnProperty('Base height')) {
            upgradedModel['Base height'] = '';
        }

        return upgradedModel;
    }

    presenter.removeShowVideoFromModel = function(model) {
        const upgradedModel = {};
        $.extend(true, upgradedModel, model);

        if (upgradedModel.hasOwnProperty("Show video")) {
            delete upgradedModel["Show video"];
        }

        return upgradedModel;
    };

    presenter.callMetadataLoadedQueue = function () {
        for (var i = 0; i &lt; presenter.metadataQueue.length; i++) {
            var queueElement = presenter.metadataQueue[i];
            queueElement.function.apply(queueElement.self, queueElement.arguments);
        }

        presenter.metadataQueue = [];
    };

    presenter.ERROR_CODES = {
        'MEDIA_ERR_ABORTED': 1,
        'MEDIA_ERR_DECODE': 2,
        'MEDIA_ERR_NETWORK': 3,
        'MEDIA_ERR_SRC_NOT_SUPPORTED': [4, 'Ups ! Looks like your browser doesn\'t support this codecs. Go &lt;a href="https://tools.google.com/dlpage/webmmf/" &gt; -here- &lt;/a&gt; to download WebM plugin'],
        'NVT01': "Not valid data format in time labels property",
        'NVD01': "Base width and height must be either positive integers or empty"
    };

    presenter.getVideoErrorMessage = function (errorCode) {
        var errorMessage = 'We are terribly sorry, but an error has occurred: ';

        switch (errorCode) {
            case presenter.ERROR_CODES.MEDIA_ERR_ABORTED:
                errorMessage += 'you aborted the video playback.';
                break;
            case presenter.ERROR_CODES.MEDIA_ERR_NETWORK:
                errorMessage += 'a network error caused the video download to fail part-way.';
                break;
            case presenter.ERROR_CODES.MEDIA_ERR_DECODE:
                errorMessage += 'the video playback was aborted due to a corruption problem or because the video used features your browser did not support.';
                break;
            case presenter.ERROR_CODES.MEDIA_ERR_SRC_NOT_SUPPORTED:
                errorMessage += 'the video could not be loaded, either because the server or network failed or because the format is not supported.';
                break;
            default:
                errorMessage += 'unknown.';
                break;
        }

        return errorMessage + ' Please refresh page.';
    };

    presenter.videoTypes = [
        {name: 'MP4 video', type: 'video/mp4'},
        {name: 'Ogg video', type: 'video/ogg'},
        {name: 'WebM video', type: 'video/webm'},
        {name: 'm3u8 video', type: 'application/x-mpegURL'}
    ];

    presenter.VIDEO_STATE = {
        STOPPED: 0,
        PLAYING: 1,
        PAUSED: 2
    };

    function fullScreenChange() {
        if (presenter.configuration.isFullScreen) {
            $(presenter.videoContainer).css({
                width: "100%",
                height: "100%"
            });

            $(presenter.videoObject).css({
                width: "100%",
                height: "100%",
                position: 'fixed',
                left: '0px',
                top: '0px'
            });

            if (presenter.configuration.defaultControls) {
                $(presenter.controlBar.getMainElement()).css('position', "fixed");
            }

            presenter.$posterWrapper.hide();

        } else {
            $(presenter.videoContainer).css({
                width: presenter.configuration.dimensions.container.width + 'px',
                height: presenter.configuration.dimensions.container.height + 'px',
                position: presenter.isHLS() ? 'absolute' : 'relative'
            });
            $(presenter.videoObject).css({
                width: presenter.configuration.dimensions.video.width + 'px',
                height: presenter.configuration.dimensions.video.height + 'px',
                position: presenter.isHLS() ? 'absolute' : 'relative'
            });

            if (presenter.configuration.defaultControls) {
                $(presenter.controlBar.getMainElement()).css('position', "absolute");
            }

        }

        $(presenter.videoObject).on("loadedmetadata", function onLoadedMeta() {
            presenter.$view.find(".poster-wrapper").show();
            $(presenter.videoObject).off("loadedmetadata");
        });

        $(presenter.videoObject).on("canplay", function onCanPlay() {
            $(presenter.videoObject).off("canplay");
        });
    }

    presenter.registerHook = function () {
        presenter.mathJaxHook = MathJax.Hub.Register.MessageHook("End Process", function mathJaxResolve(message) {
            if ($(message[1]).hasClass('ic_page')) {
                presenter.mathJaxProcessEndedDeferred.resolve();
            }
        });
    };

    presenter.setPlayerController = function (controller) {
        var mathJaxDeferred = new jQuery.Deferred();
        presenter.mathJaxProcessEndedDeferred = mathJaxDeferred;
        presenter.mathJaxProcessEnded = mathJaxDeferred.promise();
        presenter.playerController = controller;
        presenter.registerHook();

        presenter.eventBus = controller.getEventBus();
        presenter.eventBus.addEventListener('PageLoaded', this);

        var pageLoadedDeferred = new jQuery.Deferred();
        presenter.pageLoadedDeferred = pageLoadedDeferred;
        presenter.pageLoaded = pageLoadedDeferred.promise();
    };

    presenter.onEventReceived = function (eventName, eventData) {
        presenter.pageLoadedDeferred.resolve();
        if (eventData.value === 'dropdownClicked' &amp;&amp; !presenter.videoObject.playing &amp;&amp; !isTemporarilyPaused()) {
            presenter.metadadaLoaded = false;
            presenter.videoObject.load();
        }
    };

    function isTemporarilyPaused() {
        return (presenter.videoObject.paused
            &amp;&amp; presenter.videoObject.readyState &gt; 2
            &amp;&amp; presenter.videoObject.currentTime &gt; 0
            &amp;&amp; !presenter.videoObject.ended
        );
    }

    presenter.createEndedEventData = function (currentVideo) {
        return {
            source: presenter.configuration.addonID,
            item: '' + (currentVideo + 1),
            value: 'ended'
        };
    };

    presenter.formatTime = function addonVideo_formatTime(seconds) {
        if (seconds &lt; 0 || isNaN(seconds)) {
            return "00:00";
        }
        var minutes = Math.floor(seconds / 60);
        minutes = (minutes &gt;= 10) ? minutes : "0" + minutes;
        seconds = Math.floor(seconds % 60);
        seconds = (seconds &gt;= 10) ? seconds : "0" + seconds;
        return minutes + ":" + seconds;
    };

    presenter.sendTimeUpdateEvent = function Video_sendTimeUpdate(formattedTime) {
        presenter.eventBus.sendEvent('ValueChanged', {
            source: presenter.configuration.addonID,
            item: (presenter.currentMovie + 1),
            value: formattedTime
        });
    };

    presenter.sendVideoEndedEvent = function () {
        var eventData = presenter.createEndedEventData(presenter.currentMovie);
        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.stopPropagationOnClickEvent = function (e) {
        e.stopPropagation();
    };

    presenter.setMetaDataOnMetaDataLoadedEvent = function () {
        if (DevicesUtils.isFirefox()) {
            presenter.$view.find(".video-container").prepend(presenter.videoObject);
        }

        presenter.metadadaLoaded = true;
        presenter.originalVideoSize = presenter.getVideoSize(presenter.configuration.addonSize, presenter.videoObject);
        presenter.calculateCaptionsOffset(presenter.configuration.addonSize, true);

        if (presenter.configuration.defaultControls) {
            presenter.$view.find('.video-container').append(presenter.controlBar.getMainElement());
            presenter.controlBar.setMaxDurationTime(presenter.videoObject.duration);
            if (presenter.stylesBeforeFullscreen.actualTime !== -1) {
                presenter.videoObject.currentTime = presenter.stylesBeforeFullscreen.actualTime;
                presenter.play();
                presenter.stylesBeforeFullscreen.actualTime = -1;
            }
        }

        presenter.callMetadataLoadedQueue();
    };

    presenter.calculateCaptionsOffset = function (size, changeWidth) {
        var videoSize = presenter.getVideoSize(size, presenter.videoObject);

        presenter.captionsOffset.left = Math.abs(size.width - videoSize.width) / 2;
        presenter.captionsOffset.top = Math.abs(size.height - videoSize.height) / 2;

        presenter.$captionsContainer.css({
            top: presenter.captionsOffset.top,
            left: presenter.captionsOffset.left,
            position: "absolute"
        });

        if (changeWidth) {
            presenter.$captionsContainer.css({
                width: videoSize.width / presenter.captionsScale.xScale,
                height: videoSize.height / presenter.captionsScale.yScale
            });
        }
    };

    /**
     * @param  {{width: Number, height:Number}} size
     * @param  {HTMLVideoElement} video
     * @returns {{width: Number, height:Number}} calculated video size
     */
    presenter.getVideoSize = function (size, video) {
        //https://stackoverflow.com/questions/17056654/getting-the-real-html5-video-width-and-height
        var videoRatio = video.videoWidth / video.videoHeight;
        var width = size.width, height = size.height;
        var elementRatio = width / height;

        if (elementRatio &gt; videoRatio) {
            width = height * videoRatio;
        } else {
            height = width / videoRatio;
        }

        return {
            width: width,
            height: height
        };
    };

    function setVideoStateOnPlayEvent() {
        presenter.videoState = presenter.VIDEO_STATE.PLAYING;
        presenter.addClassToView('playing');
    }

    function setVideoStateOnPauseEvent() {
        if (!presenter.isHideExecuted) {
            presenter.videoState = presenter.VIDEO_STATE.PAUSED;
            presenter.removeClassFromView('playing');
        }

        delete presenter.isHideExecuted;
    }

    presenter.removeMathJaxHook = function () {
        MathJax.Hub.signal.hooks["End Process"].Remove(presenter.mathJaxHook);
    };

    presenter.onDestroy = function () {
        var view = document.getElementsByClassName('ic_page');

        if (presenter.hlsPlayer != null) {
            presenter.hlsPlayer.off('click');
            presenter.hlsPlayer.off('error');
            presenter.hlsPlayer.off('loadedmetadata');
            presenter.hlsPlayer.off('play');
            presenter.hlsPlayer.off('pause');
            presenter.hlsPlayer.off('playing');

            presenter.hlsPlayer.off('canplay');
            presenter.hlsPlayer.dispose();
            presenter.hlsPlayer = null;
        }

        if (presenter.configuration.defaultControls) {
            presenter.controlBar.destroy();
        }

        presenter.stop();

        presenter.videoObject.removeEventListener('click', presenter.stopPropagationOnClickEvent);
        presenter.videoObject.removeEventListener('loadedmetadata', presenter.setMetaDataOnMetaDataLoadedEvent);
        presenter.videoObject.removeEventListener('play', setVideoStateOnPlayEvent);
        presenter.videoObject.removeEventListener('pause', setVideoStateOnPauseEvent);
        presenter.videoObject.removeEventListener('stalled', presenter.onStalledEventHandler);
        presenter.videoObject.removeEventListener('webkitfullscreenchange', fullScreenChange);
        $(document).off('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange');
        document.removeEventListener("mozfullscreenchange", fullScreenChange);

        presenter.$videoObject.unbind("ended");
        presenter.$videoObject.unbind("error");
        presenter.$videoObject.unbind("canplay");
        presenter.$videoObject.unbind('timeupdate');

        presenter.removeMathJaxHook();
        presenter.$view.off();

        presenter.videoObject.src = '';
        presenter.mathJaxHook = null;
        presenter.eventBus = null;
        presenter.view = null;
        presenter.viewObject = null;
        presenter.videoObject = null;

        $(view).off('click');
        $(window).off('click');
    };

    presenter.resizeVideoToWindow = function () {
        var body = document.getElementsByTagName('body')[0];

        var video = presenter.videoContainer.get(0);
        presenter.stylesBeforeFullscreen.actualTime = presenter.videoObject.currentTime;
        presenter.stylesBeforeFullscreen.style = {
            position: video.style.position,
            top: video.style.top,
            left: video.style.left,
            zIndex: video.style.zIndex,
            className: video.className
        };

        presenter.stylesBeforeFullscreen.changedStyles = true;
        video.style.position = "fixed";
        video.style.top = "0";
        video.style.left = "0";
        video.style.zIndex = 20000;
        video.className = video.className + " " + presenter.$view[0].className;
        body.appendChild(video);
        presenter.metadadaLoaded = false;
        presenter.videoObject.load();
        presenter.scalePosterToWindowSize();
        presenter.scaleCaptionsContainerToVideoNewVideoSize();
    };

    presenter.scalePosterToWindowSize = presenter.metadataLoadedDecorator(function () {
        var size = {
            width: $(presenter.videoObject).width(),
            height: $(presenter.videoObject).height()
        };

        presenter.calculatePosterSize(presenter.videoObject, size);
    });

    presenter.fullScreen = function () {
        var requestMethod = requestFullscreen(presenter.videoContainer);
        presenter.stylesBeforeFullscreen.moduleWidth = presenter.$view.width();
        presenter.stylesBeforeFullscreen.moduleHeight = presenter.$view.height();
        if (requestMethod === null) {
            presenter.resizeVideoToWindow();
        } else {
            presenter.scaleCaptionsContainerToScreenSize();

            var size = {
                width: screen.width,
                height: screen.height
            };
            presenter.calculatePosterSize(presenter.videoObject, size);
        }

        presenter.configuration.isFullScreen = true;
        presenter.playerController.setAbleChangeLayout(false);
        fullScreenChange();
    };

    presenter.closeFullscreen = function () {
        if (presenter.stylesBeforeFullscreen.changedStyles === true) {
            presenter.stylesBeforeFullscreen.actualTime = presenter.videoObject.currentTime;
            presenter.stylesBeforeFullscreen.changedStyles = false;
            var video = presenter.videoContainer.get(0);
            presenter.videoView.appendChild(video);
            presenter.metadadaLoaded = false;
            presenter.videoObject.load();
            video.style.position = presenter.stylesBeforeFullscreen.style.position;
            video.style.top = presenter.stylesBeforeFullscreen.style.top;
            video.style.left = presenter.stylesBeforeFullscreen.style.left;
            video.style.zIndex = presenter.stylesBeforeFullscreen.style.zIndex;
            video.className = presenter.stylesBeforeFullscreen.style.className;
        } else {
            exitFullscreen();
        }
        presenter.configuration.isFullScreen = false;
        presenter.removeScaleFromCaptionsContainer();
        fullScreenChange();

        presenter.calculatePosterSize(presenter.videoObject, presenter.configuration.addonSize);
        presenter.playerController.setAbleChangeLayout(true);
    };

    presenter.switchAudioDescriptionEnabled = function() {
        if (presenter.isAudioDescriptionEnabled == null) {
            setAudioDescriptionEnabled(false);
        } else {
            setAudioDescriptionEnabled(!presenter.isAudioDescriptionEnabled);
        }
    };

    function setAudioDescriptionEnabled(isEnabled) {
        presenter.isAudioDescriptionEnabled = isEnabled;
        if (presenter.isAudioDescriptionEnabled) {
            speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.audioDescriptionEnabled)]);
        } else {
            speak([window.TTSUtils.getTextVoiceObject(presenter.speechTexts.audioDescriptionDisabled)]);
            setAudioDescriptionDisabled();
        }
    }

    function setAudioDescriptionDisabled(){
        if ('speechSynthesis' in window) {
            window.speechSynthesis.cancel();
        }
        if (window.responsiveVoice &amp;&amp; window.responsiveVoice.isPlaying()) {
            audioDescriptionEndedCallback();
            window.responsiveVoice.cancel();
        }
        for ( var i = 0; i &lt; presenter.descriptions.length; i++) {
            var description = presenter.descriptions[i];
            $(description.element).css('visibility', 'hidden');
            $(description.element).attr('visibility', 'hidden');
        }
    }

    presenter.showAudioDescription = function() {
        setAudioDescriptionEnabled(true);
    };

    presenter.hideAudioDescription = function() {
        setAudioDescriptionEnabled(false);
    };

    presenter.changePlayingSpeed = function(diff) {
            const playingSpeedOptions = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2];
            let currentIndex = playingSpeedOptions.indexOf(presenter.videoObject.playbackRate);
            if (currentIndex == -1) return;
            currentIndex += diff;
            if (currentIndex &lt; 0) currentIndex = 0;
            if (currentIndex &gt;= playingSpeedOptions.length) currentIndex = playingSpeedOptions.length - 1;
            presenter.videoObject.playbackRate = playingSpeedOptions[currentIndex];
        }


    presenter.keyboardController = function (keycode, isShift, event, keysDownCodes) {
        event.preventDefault();

        function increasedVolume() {
            var val = Math.round((presenter.videoObject.volume + 0.1) * 10) / 10;

            return val &gt; 1 ? 1 : val;
        }

        function decreasedVolume() {
            var val = Math.round((presenter.videoObject.volume - 0.1) * 10) / 10;

            return val &lt; 0 ? 0 : val;
        }

        function forward() {
            presenter.videoObject.currentTime += 15;
        }

        function backward() {
            presenter.videoObject.currentTime -= 15;
        }

        function playPause() {
            if (presenter.videoObject.paused) {
                presenter.play();
            } else {
                presenter.pause();
            }
        }

        function nextTimeLabel() {
            var currentTime = presenter.videoObject.currentTime;
            var currentElement = presenter.configuration.files[presenter.currentMovie],
                /**
                 * @type {{title: String, time: Number}[]}
                 */
                timeLabels = currentElement.timeLabels;


            for (var i = 0; i &lt; timeLabels.length; i++) {
                var element = timeLabels[i];

                if (element.time &gt; currentTime) {
                    presenter.seek(element.time);
                    break;
                }
            }
        }

        function previousTimeLabel() {
            var currentTime = presenter.videoObject.currentTime - 2;
            var currentElement = presenter.configuration.files[presenter.currentMovie],
                /**
                 * @type {{title: String, time: Number}[]}
                 */
                timeLabels = currentElement.timeLabels;

            for (var i = timeLabels.length - 1; i &gt;= 0; i--) {
                var element = timeLabels[i];

                if (element.time &lt; currentTime) {
                    presenter.seek(element.time);
                    break;
                }
            }
        }

        function increaseSpeed() {
            presenter.changePlayingSpeed(1);
            presenter.updateVideoSpeedController();
        }

        function decreaseSpeed() {
            presenter.changePlayingSpeed(-1);
            presenter.updateVideoSpeedController();
        }

        switch (keycode) {
            case 32:
                playPause();
                break;
            case 38:
                if (keysDownCodes.indexOf(83) &gt; -1) { // Press S at the same time
                    increaseSpeed();
                } else {
                    presenter.videoObject.volume = increasedVolume();
                }
                break;
            case 40:
                if (keysDownCodes.indexOf(83) &gt; -1) { // Press S at the same time
                    decreaseSpeed();
                } else {
                    presenter.videoObject.volume = decreasedVolume();
                }
                break;
            case 37:
                if (!isShift) {
                    backward();
                } else {
                    previousTimeLabel();
                }
                break;
            case 39:
                if (!isShift) {
                    forward();
                } else {
                    nextTimeLabel();
                }
                break;
            case 27:
                presenter.pause();
                break;
            case 70:
                presenter.fullScreen();
                break;
            case 65: // A
                presenter.switchAudioDescriptionEnabled();
                break;
            case 83: // S
                if (keysDownCodes.indexOf(38) &gt; -1) { // Press UP at the same time
                    increaseSpeed();
                } else if (keysDownCodes.indexOf(40) &gt; -1) { // Press DOWN at the same time
                    decreaseSpeed();
                }
                break;
        }
    };

    /**
     *
     * @param {String} timeLabel
     */
    presenter.validateTimeLabel = function (timeLabel, index) {
        var title = timeLabel.split(' ').slice(1).join(' '),
            time = timeLabel.split(' ')[0],
            //[Sec, Min, Hour]
            timeMultiplication = [1, 60, 60 * 60],
            timeElements = time.split(':'),
            i;

        if (timeElements.length === 0 || timeElements.length &gt; 3) {
            return {
                isValid: false,
                errorCode: "NVT01"
            };
        }

        for (i = 0; i &lt; timeElements.length; i++) {
            if (!timeElements[i].match(/^[0-9]+$/g)) {
                return {
                    isValid: false,
                    errorCode: "NVT01"
                };
            }
        }

        if (title.trim() === '') {
            title = index + ". " + time;
        }

        var timeInSeconds = 0;

        timeElements = timeElements.reverse();
        for (i = timeElements.length - 1; i &gt;= 0; i--) {
            timeInSeconds += parseInt(timeElements[i], 10) * timeMultiplication[i];
        }

        if (isNaN(timeInSeconds)) {
            return {
                isValid: false,
                errorCode: "NVT01"
            };
        }

        return {
            isValid: true,
            title: title,
            time: timeInSeconds
        };
    };

    presenter.validateTimeLabels = function (file) {
        var timeLabelsText = file['time_labels'],
            timeLabels = timeLabelsText.match(/[^\r\n]+/g) || [],  //https://stackoverflow.com/questions/5034781/js-regex-to-split-by-line
            validatedTimeLabels = [];

        for (var i = 0; i &lt; timeLabels.length; i++) {
            var validatedTimeLabel = presenter.validateTimeLabel(timeLabels[i], i + 1);
            if (!validatedTimeLabel.isValid) {
                return validatedTimeLabel;
            }

            validatedTimeLabels.push(validatedTimeLabel);
        }

        validatedTimeLabels = validatedTimeLabels.sort(function (a, b) {
            return a.time - b.time;
        });

        return {
            isValid: true,
            value: validatedTimeLabels
        }
    };

    presenter.validateFile = function (file) {
        var validatedTimeLabels = presenter.validateTimeLabels(file);
        if (!validatedTimeLabels.isValid) {
            return validatedTimeLabels;
        }

        var fileToReturn = {
            "Ogg video": file['Ogg video'],
            "MP4 video": file['MP4 video'],
            "WebM video": file['WebM video'],
            "m3u8 video": file['m3u8 video'],
            "Subtitles": file['Subtitles'],
            "Audiodescription": file['Audio Description'],
            "Poster": file['Poster'],
            "ID": file['ID'],
            "AlternativeText": file['AlternativeText'],
            "Loop video": ModelValidationUtils.validateBoolean(file['Loop video']),
            timeLabels: validatedTimeLabels.value
        };

        return {
            isValid: true,
            file: fileToReturn
        };

    };

    presenter.validateFiles = function (model) {
        var modelFiles = model.Files;
        var files = [];

        for (var i = 0; i &lt; modelFiles.length; i++) {
            var validatedFile = presenter.validateFile(modelFiles[i]);
            if (!validatedFile.isValid) {
                return validatedFile;
            }

            files.push(validatedFile.file);
        }

        return {
            isValid: true,
            files: files
        };
    };

    function getSpeechTextProperty (rawValue, defaultValue) {
        var value = rawValue.trim();

        if (value === undefined || value === null || value === '') {
            return defaultValue;
        }

        return value;
    }

    function setSpeechTexts (speechTexts) {
        presenter.speechTexts = {
            audioDescriptionEnabled:  'Audio description enabled',
            audioDescriptionDisabled: 'Audio description disabled'
        };

        if (!speechTexts) {
            return;
        }

        presenter.speechTexts = {
            audioDescriptionEnabled: getSpeechTextProperty(speechTexts['AudioDescriptionEnabled']['AudioDescriptionEnabled'], presenter.speechTexts.audioDescriptionEnabled),
            audioDescriptionDisabled: getSpeechTextProperty(speechTexts['AudioDescriptionDisabled']['AudioDescriptionDisabled'], presenter.speechTexts.audioDescriptionDisabled)
        };
    }

    presenter.validateModel = function (model) {
        var validatedFiles = presenter.validateFiles(model);
        setSpeechTexts(model["speechTexts"]);
        if (!validatedFiles.isValid) {
            return validatedFiles;
        }

        var baseDimensions = {
            width: 0,
            height: 0
        };
        if (model['Base width'].trim().length !== 0) {
            var validatedBaseWidth = ModelValidationUtils.validatePositiveInteger(model['Base width']);
            if (validatedBaseWidth.isValid) {
                baseDimensions.width = validatedBaseWidth.value;
            } else {
                validatedBaseWidth.errorCode = 'NVD01';
                return validatedBaseWidth;
            }
        }
        if (model['Base height'].trim().length !== 0) {
            var validatedBaseHeight = ModelValidationUtils.validatePositiveInteger(model['Base height']);
            if (validatedBaseHeight.isValid) {
                baseDimensions.height = validatedBaseHeight.value;
            } else {
                validatedBaseHeight.errorCode = 'NVD01';
                return validatedBaseHeight;
            }
        }

        return {
            isValid: true,
            addonSize: {
                width: parseInt(model.Width, 10),
                height: parseInt(model.Height, 10)
            },
            addonID: model.ID,
            isVisibleByDefault: ModelValidationUtils.validateBoolean(model["Is Visible"]),
            shouldHideSubtitles: ModelValidationUtils.validateBoolean(model["Hide subtitles"]),
            defaultControls: !ModelValidationUtils.validateBoolean(model['Hide default controls']),
            files: validatedFiles.files,
            originalFiles: JSON.parse(JSON.stringify(validatedFiles.files)), //deep copy
            height: parseInt(model.Height, 10),
            showPlayButton: ModelValidationUtils.validateBoolean(model['Show play button']),
            isTabindexEnabled: ModelValidationUtils.validateBoolean(model["Is Tabindex Enabled"]),
            offlineMessage: model["offlineMessage"],
            enableVideoSpeedController: ModelValidationUtils.validateBoolean(model["enableVideoSpeedController"]),
            baseDimensions: baseDimensions
        }
    };

    presenter.checkPlayButtonVisibility = function () {
        if (!presenter.configuration.showPlayButton) {
            presenter.$view.find('.video-poster-play').hide();
        }
    };

    presenter.cachePosters = function () {
        for (var fileNumber = 0; fileNumber &lt; presenter.configuration.files.length; fileNumber++) {
            presenter.cachePoster(fileNumber);
        }
    };

    presenter.cachePoster = function (fileNumber) {
        var posterSource = presenter.configuration.files[fileNumber].Poster;
        if (typeof posterSource == 'object') return; //don't cache twice
        if (posterSource) {
            var image = new Image();
            image.src = posterSource;

            presenter.configuration.files[fileNumber].Poster = image;
        }
    };


    presenter.showPlayButton = function () {
        if (presenter.configuration.showPlayButton) {
            presenter.posterPlayButton.show();
        }
    };

    presenter.hidePlayButton = function () {
        if (presenter.configuration.showPlayButton) {
            presenter.posterPlayButton.hide();
        }
    };

    presenter.setBurgerMenu = function () {
        var BURGER_MENU = "time_labels";
        if (!presenter.configuration.defaultControls) {
            return;
        }

        presenter.controlBar.removeBurgerMenu(BURGER_MENU);

        var currentElement = presenter.configuration.files[presenter.currentMovie],
            /**
             * @type {{title: String, time: Number}[]}
             */
            labels = currentElement.timeLabels;

        if (labels.length === 0) {
            return;
        }

        var elementsForBurger = labels.map(function (value) {
            return {
                title: value.title,
                callback: function () {
                    presenter.seek(value.time);
                }
            };
        });

        presenter.controlBar.addBurgerMenu(BURGER_MENU, elementsForBurger);
    };

    presenter.addVideoSpeedController = function () {
        if (!presenter.configuration.defaultControls || !presenter.configuration.enableVideoSpeedController) {
            return;
        }

        presenter.isVideoSpeedControllerAdded = true;
        presenter.controlBar.addVideoSpeedController(presenter.setPlaybackRate);
    };

    presenter.resetVideoSpeedController = function () {
        if (!presenter.configuration.defaultControls || !presenter.configuration.enableVideoSpeedController) {
            return;
        }

        presenter.controlBar.resetPlaybackRateSelectValue();
        presenter.setPlaybackRate(1.0);
    };

    presenter.run = function (view, model) {
        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);
        if (!validatedModel.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.errorCode);
            return;
        }

        presenter.configuration = $.extend(presenter.configuration, validatedModel);

        presenter.cachePosters();

        presenter.videoState = presenter.VIDEO_STATE.STOPPED;
        presenter.videoView = view;
        presenter.$view = $(view);

        presenter.posterPlayButton = $(view).find('.video-poster-play');
        presenter.videoContainer = $(view).find('.video-container:first');
        presenter.$captionsContainer = presenter.$view.find(".captions-container:first");
        presenter.$posterWrapper = presenter.$view.find('.poster-wrapper');
        presenter.$mask = presenter.$view.find('.video-container-mask');

        presenter.videoObject = presenter.videoContainer.find('video')[0];
        presenter.$videoObject = $(presenter.videoObject);

        Object.defineProperty(presenter.videoObject, 'playing', {
           get: function () {
               return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
           }
        });

        presenter.setDimensions();

        presenter.checkPlayButtonVisibility();

        if (presenter.configuration.defaultControls) {
            presenter.buildControlsBars();
        } else {
            presenter.videoContainer.on("click", function () {
                presenter.videoObject = presenter.videoContainer.find('video')[0];
                if (presenter.videoObject.paused) {
                    presenter.play();
                } else {
                    presenter.pause();
                }
            });
        }

        presenter.addTabindex(presenter.configuration.isTabindexEnabled);

        if (presenter.isHLS()) presenter.loadHLSPlayer();

        presenter.connectHandlers();
        presenter.reload();

        if (!presenter.configuration.isVisibleByDefault) {
            presenter.hide();
        }

        presenter.eventBus.addEventListener('ValueChanged', this);

        if (presenter.configuration.shouldHideSubtitles) {
            presenter.hideSubtitles();
        } else {
            presenter.showSubtitles();
        }

        presenter.videoObject.setAttribute('webkit-playsinline', 'webkit-playsinline');
        presenter.videoObject.setAttribute('playsinline', 'playsinline');
    };

    presenter.connectHandlers = function () {
        if (!presenter.isHLS()) {
            presenter.videoObject.addEventListener('click', presenter.stopPropagationOnClickEvent);
            presenter.videoObject.addEventListener('error', function () {
                presenter.handleErrorCode(this.error);
            }, true);
            presenter.videoObject.addEventListener('loadedmetadata', presenter.setMetaDataOnMetaDataLoadedEvent);
            presenter.videoObject.addEventListener('play', setVideoStateOnPlayEvent);
            presenter.videoObject.addEventListener('pause', setVideoStateOnPauseEvent);
            presenter.videoObject.addEventListener('playing', presenter.onVideoPlaying, false);
        }
        $(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange', presenter.fullscreenChangedEventReceived);

        presenter.addClickListener();
    };

    presenter.addClickListener = function () {
        var view = document.getElementsByClassName('ic_page');
        $(view[0]).on('click', function (event) {
            if (presenter.configuration.defaultControls &amp;&amp; presenter.controlBar.isSelectorOpen &amp;&amp; !event.target.localName.includes('select')) {
                presenter.controlBar.isSelectorOpen = false;
                presenter.controlBar.hideControls();
            }
            event.preventDefault();
            event.stopPropagation();
        });

        $(window).on('click', function (event) {
            if (presenter.configuration.defaultControls &amp;&amp; presenter.controlBar.isSelectorOpen) {
                presenter.controlBar.isSelectorOpen = false;
                presenter.controlBar.hideControls();
            }
            event.preventDefault();
            event.stopPropagation();
        });
    }

    presenter.isHLS = function() {
        if (window.videojs === undefined) return false;
        if (presenter.isHLSValue == null) {
            presenter.isHLSValue = false;
            for (var i = 0; i &lt; presenter.configuration.files.length; i++) {
                var videoFile = presenter.configuration.files[i];
                if (videoFile["m3u8 video"].trim().length &gt; 0) presenter.isHLSValue = true;
            }
        }
        return presenter.isHLSValue;
    }

    presenter.loadHLSPlayer = function() {
        presenter.$videoObject.addClass('video-js vjs-theme-sea vjs-big-play-centered');
        presenter.$videoObject.attr({
            preload: presenter.isHLS() ? 'metadata' : 'auto',
            fluid: "true",
            "data-setup": '{}'
        });
        var videoID = 'videojs-player-' + presenter.configuration.addonID;
        presenter.$videoObject.attr('id', videoID);
        presenter.hlsPlayer = window.videojs(videoID);
        presenter.hlsPlayer.ready(() =&gt; {
            presenter.hlsPlayer.on('click', presenter.stopPropagationOnClickEvent);
            presenter.hlsPlayer.on('error', function () {
                presenter.handleErrorCode(this.error);
            });
            presenter.hlsPlayer.on('loadedmetadata', presenter.setMetaDataOnMetaDataLoadedEvent);
            presenter.hlsPlayer.on('play', setVideoStateOnPlayEvent);
            presenter.hlsPlayer.on('pause', setVideoStateOnPauseEvent);
            presenter.hlsPlayer.on('playing', presenter.onVideoPlaying);

            presenter.hlsPlayer.on('loadedmetadata', function onLoadedMetadata() {
                presenter.isVideoLoaded = true;
                presenter.callTasksFromDeferredQueue();

                $(presenter.videoObject).unbind("canplay");

                if (presenter.areSubtitlesHidden) {
                    presenter.hideSubtitles();
                } else {
                    presenter.showSubtitles();
                }
                }
            );
        });
    }

    presenter.fullscreenChangedEventReceived = function () {
        if (!isVideoInFullscreen() &amp;&amp; presenter.configuration.isFullScreen) {
            presenter.configuration.isFullScreen = false;
            presenter.removeScaleFromCaptionsContainer();
            fullScreenChange();
            presenter.configuration.defaultControls &amp;&amp; presenter.controlBar.showFullscreenButton();

            presenter.calculatePosterSize(presenter.videoObject, presenter.configuration.addonSize);
            presenter.playerController.setAbleChangeLayout(true);
        }
    };

    presenter.checkAddonSize = function () {
        if (presenter.videoContainer.width() !== presenter.lastWidthAndHeightValues.width
            || presenter.videoContainer.height() !== presenter.lastWidthAndHeightValues.height) {

            presenter.lastWidthAndHeightValues.width = presenter.videoContainer.width();
            presenter.lastWidthAndHeightValues.height = presenter.videoContainer.height();

            presenter.calculateCaptionsOffset(presenter.lastWidthAndHeightValues, false);
            presenter.scaleCaptionsContainerToVideoNewVideoSize();
        }
    };

    presenter.buildControlsBars = function () {
        var config = {
            videoObject: presenter.videoObject,
            parentElement: presenter.videoContainer[0],
            isVolumeEnabled: !MobileUtils.isSafariMobile(navigator.userAgent)
        };

        var controls = new window.CustomControlsBar(config);

        controls.addPlayCallback(presenter.play);
        controls.addPauseCallback(presenter.pause);
        controls.addStopCallback(presenter.stop);
        controls.addFullscreenCallback(presenter.fullScreen);
        controls.addCloseFullscreenCallback(presenter.closeFullscreen);
        controls.addProgressChangedCallback(presenter.seekFromPercent);
        controls.addVolumeChangedCallback(presenter.setVolume);
        controls.addCallbackToBuildInTimer(presenter.checkAddonSize);

        presenter.$view.find('.video-container').append(controls.getMainElement());

        presenter.controlBar = controls;
    };

    presenter.scaleCaptionsContainerToVideoNewVideoSize = presenter.metadataLoadedDecorator(function () {
        var size = {
            width: $(presenter.videoObject).width(),
            height: $(presenter.videoObject).height()
        };

        var newVideoSize = presenter.getVideoSize(size, presenter.videoObject);

        var xScale = newVideoSize.width / presenter.originalVideoSize.width;
        var yScale = newVideoSize.height / presenter.originalVideoSize.height;

        if (presenter.configuration.baseDimensions.width != 0) {
            xScale = xScale * (presenter.configuration.addonSize.width/presenter.configuration.baseDimensions.width);
        }
        if (presenter.configuration.baseDimensions.height != 0) {
            yScale = yScale * (presenter.configuration.addonSize.height/presenter.configuration.baseDimensions.height);
        }

        presenter.$captionsContainer.css(generateTransformDict(xScale, yScale));
        presenter.captionsScale.xScale = xScale;
        presenter.captionsScale.yScale = yScale;

        presenter.calculateCaptionsOffset(size, true);
    });

    presenter.scaleCaptionsContainerToScreenSize = presenter.metadataLoadedDecorator(function () {
        var size = {
            width: screen.width,
            height: screen.height
        };

        var newVideoSize = presenter.getVideoSize(size, presenter.videoObject);

        var xScale = newVideoSize.width / presenter.originalVideoSize.width;
        var yScale = newVideoSize.height / presenter.originalVideoSize.height;


        presenter.$captionsContainer.css(generateTransformDict(xScale, yScale));
        presenter.captionsScale.xScale = xScale;
        presenter.captionsScale.yScale = yScale;

        presenter.calculateCaptionsOffset(size, false);
    });

    presenter.removeScaleFromCaptionsContainer = presenter.metadataLoadedDecorator(function () {
        presenter.$captionsContainer.css(generateTransformDict(1, 1));
        presenter.captionsScale.xScale = 1.0;
        presenter.captionsScale.yScale = 1.0;

        presenter.calculateCaptionsOffset(presenter.configuration.addonSize, false);
    });

    presenter.sendOnPlayingEvent = function () {
        var eventData = {
            'source': presenter.configuration.addonID,
            'item': (presenter.currentMovie + 1),
            'value': 'playing',
            'score': ''
        };

        presenter.eventBus.sendEvent('ValueChanged', eventData);
    };

    presenter.onVideoPlaying = function AddonVideo_onVideoPlaying() {
        presenter.sendOnPlayingEvent();

        if (presenter.videoObject.currentTime === 0) {
            presenter.sendTimeUpdateEvent(presenter.formatTime(presenter.videoObject.currentTime))
        }
    };

    presenter.convertTimeStringToNumber = function (timeString) {
        timeString = timeString.split(':');
        var minutes = parseInt(timeString[0] * 60, 10);
        var seconds = parseInt(timeString[1], 10);
        return {isCorrect: true, value: (minutes + seconds)};
    };

    presenter.handleErrorCode = function (error) {
        if (!error) return;

        presenter.$view.html(presenter.getVideoErrorMessage(error.code));
    };

    presenter.createPreview = function (view, model) {
        presenter.isPreview = true;

        var upgradedModel = presenter.upgradeModel(model);
        var validatedModel = presenter.validateModel(upgradedModel);
        if (!validatedModel.isValid) {
            DOMOperationsUtils.showErrorMessage(view, presenter.ERROR_CODES, validatedModel.errorCode);
            return;
        }

        presenter.configuration = $.extend(presenter.configuration, validatedModel);

        presenter.$view = $(view);
        presenter.videoContainer = $(view).find('.video-container:first');

        presenter.setVideo();
        presenter.setDimensions();
    };

    presenter.showCaptions = function (time) {
        if (!presenter.configuration.dimensions) return ; // No captions to show when video wasn't loaded properly
        for (var i = 0; i &lt; presenter.captions.length; i++) {
            var caption = presenter.captions[i];
            if (caption.start &lt;= time &amp;&amp; caption.end &gt;= time) {
                $(caption.element).attr('visibility', 'visible');
                $(caption.element).css('visibility', presenter.isCurrentlyVisible ? 'visible' : 'hidden');
            } else {
                $(caption.element).css('visibility', 'hidden');
                $(caption.element).attr('visibility', 'hidden');
            }
        }
    };

    function getAudioDescriptionEnabled() {
        if (presenter.isAudioDescriptionEnabled != null) {
            return presenter.isAudioDescriptionEnabled;
        }
        if (presenter.playerController &amp;&amp; presenter.playerController.isWCAGOn()) {
            return presenter.playerController.isWCAGOn();
        }
        return false;
    }

    function audioDescriptionEndedCallback() {
        if (presenter) {
            if(!presenter.usedStop) {
                presenter.play();
            }
        }
    }

    presenter.readAudioDescriptions = function (time) {
        if (!presenter.configuration.dimensions) return false;
        if (!presenter.playerController || !getAudioDescriptionEnabled()) return false;
        if ((time &lt; presenter.prevTime) || ((time - presenter.prevTime) &gt; 1.0)) {
            presenter.prevTime = time - 0.001;
            return false;
        }

        var isSpeaking = false;
        for ( var i = 0; i &lt; presenter.descriptions.length; i++) {
            var description = presenter.descriptions[i];
            if (presenter.prevTime &lt; description.start &amp;&amp; description.start &lt;= time) {
                isSpeaking = true;
                presenter.pause();
                $(description.element).attr('visibility', 'visible');
                $(description.element).css('visibility', presenter.isCurrentlyVisible ? 'visible' : 'hidden');
                speakWithCallback([window.TTSUtils.getTextVoiceObject(description.text,description.langTag)], audioDescriptionEndedCallback);
            } else {
                $(description.element).css('visibility', 'hidden');
                $(description.element).attr('visibility', 'hidden');
            }
        }

        presenter.prevTime = time;

        if (isSpeaking) {
            for (var i = 0; i &lt; presenter.captions.length; i++) {
                var caption = presenter.captions[i];
                $(caption.element).css('visibility', 'hidden');
                $(caption.element).attr('visibility', 'hidden');
            }
        }
        return isSpeaking;
    };

    presenter.reload = function () {
        presenter.cachePosters();
        presenter.showPlayButton();
        presenter.isVideoLoaded = false;
        $(presenter.videoContainer).find('.captions').remove();
        presenter.setVideo();
        presenter.loadSubtitles();
        presenter.loadAudioDescription();
        presenter.setBurgerMenu();
        if (presenter.isVideoSpeedControllerAdded) {
            presenter.resetVideoSpeedController();
        } else {
            presenter.addVideoSpeedController();
        }
        $(presenter.videoObject).unbind('timeupdate');
        $(presenter.videoObject).bind("timeupdate", function () {
            onTimeUpdate(this);
        });
        presenter.removeClassFromView('playing');
        presenter.posterPlayButton.removeClass('video-poster-pause');
    };

    presenter.sendTimeUpdate = function Video_sendTime() {
        var actualVideoTime = parseInt(presenter.videoObject.currentTime, 10);
        if (actualVideoTime !== presenter.lastSentCurrentTime) {
            var formattedTime = presenter.formatTime(actualVideoTime, 10);
            presenter.sendTimeUpdateEvent(formattedTime);
            presenter.lastSentCurrentTime = actualVideoTime;
        }
    };

    function onTimeUpdate(video) {
        if (!presenter.videoObject) return;
        if (!presenter.videoObject.paused) {
            var isSpeaking = presenter.readAudioDescriptions(presenter.videoObject.currentTime);
            if (!isSpeaking) {
                presenter.showCaptions(presenter.videoObject.currentTime);
            }
        }

        presenter.sendTimeUpdate();

        var currentTime = Math.round(video.currentTime * 10) / 10,
            videoDuration = Math.round(video.duration * 10) / 10,
            isFullScreen = document.mozFullScreen || document.webkitIsFullScreen;

        var shouldSetAbleChangeLayout = false;
        if (currentTime &gt;= videoDuration) {
            presenter.sendVideoEndedEvent();
            presenter.showWaterMark();
            presenter.prevTime = -0.001;
            if (document.exitFullscreen &amp;&amp; document.fullscreenElement) {
                document.exitFullscreen();
            } else if (document.webkitExitFullscreen) {
                document.webkitExitFullscreen();
            } else if (document.mozCancelFullScreen) {
                document.mozCancelFullScreen();
            } else if (document.msExitFullscreen) {
                document.msExitFullscreen();
            } else if (presenter.configuration.isFullScreen) {
                presenter.configuration.isFullScreen = false;
                presenter.removeScaleFromCaptionsContainer();
                presenter.configuration.defaultControls &amp;&amp; presenter.controlBar.showFullscreenButton();
                presenter.closeFullscreen();
                shouldSetAbleChangeLayout = true;

            }

            if (!presenter.configuration.defaultControls) {
                presenter.seek(0); // sets the current time to 0
                presenter.$posterWrapper.show();
                if (presenter.configuration.showPlayButton) {
                    presenter.posterPlayButton.show();
                }
                presenter.videoObject.pause();
            }

            $(presenter.videoObject).on("canplay", function onCanPlay() {
                currentTime = 0;
                presenter.videoObject.currentTime = currentTime;
                presenter.pause();
                $(presenter.videoObject).off("canplay");
            });

            presenter.lastSentCurrentTime = 0;

            if(shouldSetAbleChangeLayout) {
                presenter.playerController.setAbleChangeLayout(true);
            }
        }
    }

    presenter.getState = function () {
        var isPaused = presenter.videoObject.paused;
        return JSON.stringify({
            files: "deprecated",        //Removed from state.
            videoURLS: presenter.addedVideoURLS,
            currentTime: presenter.videoObject.currentTime,
            isCurrentlyVisible: presenter.isCurrentlyVisible,
            isPaused: isPaused,
            currentMovie: presenter.currentMovie,
            areSubtitlesHidden: presenter.areSubtitlesHidden,
            isAudioDescriptionEnabled: presenter.isAudioDescriptionEnabled
        });
    };

    presenter.setState = function (stateString) {
        if (ModelValidationUtils.isStringEmpty(stateString)) {
            return;
        }
        var state = JSON.parse(stateString);
        var currentTime = state.currentTime;

        if (state.videoURLS) {  //This was added later than rest of state
            for (var i in state.videoURLS) {
                if (state.videoURLS.hasOwnProperty(i)) {
                    var element = state.videoURLS[i];

                    if (element.url === undefined) {
                        console.warn(`Impact of execution addon' "setVideoURL" command was not properly restored for addon with ID: ${presenter.configuration.addonID}`)
                        continue;
                    }

                    presenter._setVideoURL(element.url, element.index);
                }
            }
        }

        presenter.isCurrentlyVisible = state.isCurrentlyVisible;

        if (presenter.isCurrentlyVisible !== (presenter.$view.css('visibility') !== 'hidden')) {
            presenter.setVisibility(presenter.isCurrentlyVisible);
        }

        presenter.currentMovie = state.currentMovie;
        presenter.reload();

        var eventType = presenter.isHLS() ? 'loadedmetadata' : 'canplay';
        $(presenter.videoObject).on(eventType, function onVideoCanPlay() {
            if (presenter.videoObject.currentTime &lt; currentTime) {
                presenter.currentTime = currentTime;
                presenter.videoObject.currentTime = currentTime;
                presenter.startTime = currentTime;
                presenter.videoState = presenter.VIDEO_STATE.PAUSED;
                $(this).off('canplay');
            }

            if (state.areSubtitlesHidden != undefined) {
                if (state.areSubtitlesHidden) {
                    presenter.hideSubtitles();
                } else {
                    presenter.showSubtitles();
                }
            }
        });

        presenter.isAudioDescriptionEnabled = state.isAudioDescriptionEnabled;
    };

    presenter.getIOSVersion = function (userAgent) {
        var match = /CPU OS ([\d_]+) like Mac OS X/.exec(userAgent);
        return match === null ? '' : match[1];
    };

    /**
     * Setting poster for video.
     *
     * Attribute poster is not used because safari wont reload poster while reloading video.
     * @param  {HTMLVideoElement} video
     * @param  {String} posterSource
     */
    presenter.addAttributePoster = presenter.metadataLoadedDecorator(function (video, poster) {
        presenter.$posterWrapper.find("img").remove();
        var $video = $(video);

        if (poster) {
            presenter.$posterWrapper.prepend(poster);

            presenter.calculatePosterSize(video, presenter.configuration.addonSize);

            presenter.$posterWrapper.show();
        } else {
            presenter.$posterWrapper.hide();
            $video.attr('poster', '');
        }
    });

    presenter.calculatePosterSize = presenter.metadataLoadedDecorator(function (video, toSize) {
        var $poster = presenter.$posterWrapper.find("img");

        var calculatedVideoSize = presenter.getVideoSize(toSize, video);

        var left = (toSize.width - calculatedVideoSize.width) / 2;
        var top = (toSize.height - calculatedVideoSize.height) / 2;

        $poster.width(calculatedVideoSize.width);
        $poster.height(calculatedVideoSize.height);
        $poster.css({
            left: left,
            top: top
        });
    });

    presenter.setAltText = function () {
        var files = presenter.configuration.files;
        presenter.$view.find('.video-container-mask').text(files[presenter.currentMovie].AlternativeText);
        presenter.$view.find('.video-container-video').text(files[presenter.currentMovie].AlternativeText);
    };

    presenter.isOnlineResourceOnly = function() {
        for (var i = 0; i &lt; presenter.configuration.files.length; i++) {
            var videoFile = presenter.configuration.files[i];
            var isMP4Local = videoFile["MP4 video"] &amp;&amp; videoFile["MP4 video"].trim().indexOf("file:/") == 0;
            var isOggLocal = videoFile["Ogg video"] &amp;&amp; videoFile["Ogg video"].trim().indexOf("file:/") == 0;
            var isWebMLocal = videoFile["WebM video"] &amp;&amp; videoFile["WebM video"].trim().indexOf("file:/") == 0;
            if (!isMP4Local &amp;&amp; !isOggLocal &amp;&amp; !isWebMLocal) {
                return true;
            }
            return false;
        }
    };

    presenter.setVideo = function () {
        if (!window.navigator.onLine) {
            isOnlineResourceOnly().then(function (responses){
                setVideo();
            }).catch(function(error){
                presenter.$view.html(presenter.configuration.offlineMessage);
            });
        } else {
            setVideo();
        }
    };

    function isOnlineResourceOnly() {
        let endpointsToFetch = [];
        for (let i = 0; i &lt; presenter.configuration.files.length; i++) {
            const videoFile = presenter.configuration.files[i];
            !!videoFile["MP4 video"] &amp;&amp; endpointsToFetch.push(videoFile["MP4 video"].trim());
            !!videoFile["Ogg video"] &amp;&amp; endpointsToFetch.push(videoFile["Ogg video"].trim());
            !!videoFile["WebM video"] &amp;&amp; endpointsToFetch.push(videoFile["WebM video"].trim());
        }
        return Promise.all(endpointsToFetch.map(function(fetchURL) {
            // method: "Head" - used to check if addon have a connection to video (not for download video)
            // method: "no-cache" - mLibro has a cache. Added to avoid the situation that once there is video and
            //      once there is not, when user is offline. Such behavior would be inconsistent with the documentation.
            return fetch(fetchURL, {method: "Head", cache: "no-cache"});
        }));
    }

    function setVideo() {
        if (presenter.videoObject) {
            $(presenter.videoObject).unbind("ended");
            $(presenter.videoObject).unbind("error");
            $(presenter.videoObject).unbind("canplay");

            presenter.videoObject.pause();
        }

        presenter.videoObject = presenter.videoContainer.find('video')[0];
        if (!presenter.videoObject) return;
        if (presenter.isHLS() &amp;&amp; !!presenter.controlBar &amp;&amp; !!presenter.controlBar.configuration) {
            presenter.controlBar.configuration.videoObject = presenter.videoObject;
        }
        if (!presenter.videoObject.hasOwnProperty('playing')) {
            Object.defineProperty(presenter.videoObject, 'playing', {
               get: function () {
                   return !!(this.currentTime &gt; 0 &amp;&amp; !this.paused &amp;&amp; !this.ended &amp;&amp; this.readyState &gt; 2);
               }
            });
        }

        presenter.videoState = presenter.VIDEO_STATE.STOPPED;
        var $video = $(presenter.videoObject);
        var files = presenter.configuration.files;

        presenter.videoContainer.find('source').remove();
        presenter.addAttributePoster($video[0], files[presenter.currentMovie].Poster);

        if (!presenter.isHLS()) presenter.setAltText();
        if (presenter.isPreview) {
            $video.attr('preload', 'none');
        } else {
            $video.attr('preload', presenter.isHLS() ? 'metadata' : 'auto');
            var isNewHLSSourceSet = false;
            for (var vtype in presenter.videoTypes) {
                if (files[presenter.currentMovie][presenter.videoTypes[vtype].name] &amp;&amp; (presenter.videoObject.canPlayType(presenter.videoTypes[vtype].type) || presenter.videoTypes[vtype].type == "application/x-mpegURL")) {
                    var source = $('&lt;source&gt;');
                    source.attr('type', presenter.videoTypes[vtype].type);
                    source.attr('src', files[presenter.currentMovie][presenter.videoTypes[vtype].name]);
                    $video.append(source);
                    if (presenter.hlsPlayer != null &amp;&amp; !isNewHLSSourceSet) {
                        isNewHLSSourceSet = true;
                        presenter.hlsPlayer.src({
                            'type': presenter.videoTypes[vtype].type,
                            'src': files[presenter.currentMovie][presenter.videoTypes[vtype].name]
                        });
                    }
                }
            }

            // "ended" event doesn't work on Safari
            $(presenter.videoObject).unbind('timeupdate');
            $(presenter.videoObject).bind("timeupdate", function () {
                onTimeUpdate(presenter);
            });

            $(presenter.videoObject).bind("error", function onError() {
                $(this).unbind("error");
                presenter.reload();
                if (presenter.configuration.isFullScreen) {
                    fullScreenChange();
                }
            });

            $(presenter.videoObject).bind("canplay", function onCanPlay() {
                presenter.isVideoLoaded = true;
                presenter.callTasksFromDeferredQueue();

                $(this).unbind("canplay");

                if (presenter.areSubtitlesHidden) {
                    presenter.hideSubtitles();
                } else {
                    presenter.showSubtitles();
                }
            });
            // Android devices have problem with loading content.
            presenter.videoObject.addEventListener("stalled", presenter.onStalledEventHandler, false);
            presenter.videoObject.load();
            presenter.metadadaLoaded = false;

            if (files[presenter.currentMovie]['Loop video']) {
                if (typeof presenter.videoObject.loop == 'boolean') {
                    presenter.videoObject.loop = true;
                } else {
                    $(presenter.videoObject).on('ended', function () {
                        presenter.currentTime = 0;
                        presenter.play();
                    }, false);
                }

                presenter.isAborted = false;

                $(presenter.videoObject).on('abort', function () {
                    presenter.isAborted = true;
                });

                $(presenter.videoObject).on('canplay', function () {
                    if (presenter.isAborted &amp;&amp; presenter.playTriggered) {
                        presenter.play();
                    }
                });
            } else {
                if (typeof presenter.videoObject.loop == 'boolean') {
                    presenter.videoObject.loop = false;
                } else {
                    $(presenter.videoObject).off('ended');
                }
            }
        }
    }

    /**
     * Creates DIV element containing caption text.
     *
     * @param caption - used text, top and left properties
     * @return reference do newly created element
     */
    function createCaptionElement(caption, isAudioDescription) {
        const captionElement = document.createElement('div');

        $(captionElement).addClass('captions');
        if(isAudioDescription) {
            $(captionElement).addClass('audio-description');
        }
        $(captionElement).addClass(caption.cssClass);
        const sanitizedText = window.xssUtils.sanitize(caption.text);
        $(captionElement).html(window.TTSUtils.parsePreviewAltText(sanitizedText));
        $(captionElement).css({
            top: caption.top,
            left: caption.left
        });

        $(captionElement).css('visibility', 'hidden');
        $(captionElement).attr('visibility', 'hidden');
        presenter.$captionsContainer.append(captionElement);

        return captionElement;
    }

    presenter.convertLinesToCaptions = function (lines) {
        presenter.captions = [];

        for (var i = 0; i &lt; lines.length; i++) {
            var parts = lines[i].split('|');
            if (parts.length == 6) {
                var caption = {
                    start: parts[0],
                    end: parts[1],
                    top: (StringUtils.endsWith(parts[2], 'px') ? parts[2] : parts[2] + 'px'),
                    left: (StringUtils.endsWith(parts[3], 'px') ? parts[3] : parts[3] + 'px'),
                    cssClass: parts[4],
                    text: parts[5]
                };

                caption.element = createCaptionElement(caption, false);
                presenter.captions.push(caption);

                presenter.captionDivs.push(caption.element);
            }
        }
    };

    presenter.loadSubtitles = function () {
        var subtitlesLoadedDeferred = new $.Deferred(),
            subtitles = presenter.configuration.files[presenter.currentMovie].Subtitles;

        if (subtitles) {
            if (StringUtils.startsWith(subtitles, "/file")) {
                $.get(subtitles, function (data) {
                    subtitlesLoadedDeferred.resolve(data);
                });
            } else {
                subtitlesLoadedDeferred.resolve(subtitles);
            }

            presenter.convertLinesToCaptions(Helpers.splitLines(subtitles));
            $.when(subtitlesLoadedDeferred.promise(), presenter.mathJaxProcessEnded, presenter.pageLoaded).then(function onSubtitlesLoaded(data) {
                presenter.convertLinesToCaptions(Helpers.splitLines(data));
                MathJax.Hub.Queue(["Typeset", MathJax.Hub, presenter.captionDivs])();
            });
        }
    };

    presenter.loadAudioDescription = function () {
        var descriptionsLoadedDeferred = new $.Deferred(),
            descriptions = presenter.configuration.files[presenter.currentMovie].Audiodescription;

        if (descriptions) {
            if (StringUtils.startsWith(descriptions, "/file")) {
                $.get(descriptions, function (data) {
                    descriptionsLoadedDeferred.resolve(data);
                });
            } else {
                descriptionsLoadedDeferred.resolve(descriptions);
            }

            presenter.convertLinesToAudioDescriptions(Helpers.splitLines(descriptions));
            $.when(descriptionsLoadedDeferred.promise(), presenter.mathJaxProcessEnded, presenter.pageLoaded).then(function onDescriptionsLoaded(data) {
                presenter.convertLinesToAudioDescriptions(Helpers.splitLines(data));
                MathJax.Hub.Queue(["Typeset", MathJax.Hub, presenter.descriptionsDivs])();
            });
        }
    };

    presenter.escapeAltText = function(text) {
        function replacer(match, p1, offset, string) {
          return '[' + p1.replace(/\|/g, escapedSeparator) + ']';
        }
        return text.replace(/\[(.*?)\]/g, replacer);
    };
    
    presenter.unescapeAndConvertAltText = function(text) {
        function replacer(match, p1, offset, string) {
          var parts = p1.split(escapedSeparator);
          if (parts.length === 2) {
              return '\\alt{' + parts[0] + '|' + parts[1] + '}';
          }
          if (parts.length === 3) {
              return '\\alt{' + parts[0] + '|' + parts[1] + '}[lang ' + parts[2] + ']';
          }
          return '[' + parts.join('|') + ']';
        }
        return text.replace(/\[(.*?)\]/g, replacer);
    };

    presenter.convertLinesToAudioDescriptions = function (lines) {
        presenter.descriptions = [];

        for (var i = 0; i &lt; lines.length; i++) {
            var line = presenter.escapeAltText(lines[i]);
            var parts = line.split('|');
            if (parts.length == 6) {
                var description = {
                    start: parts[0],
                    top: (StringUtils.endsWith(parts[2], 'px') ? parts[1] : parts[1] + 'px'),
                    left: (StringUtils.endsWith(parts[3], 'px') ? parts[2] : parts[2] + 'px'),
                    cssClass: parts[3],
                    langTag: parts[4],
                    text: presenter.unescapeAndConvertAltText(parts[5])
                };

                description.element = createCaptionElement(description, true);
                presenter.descriptions.push(description);

                presenter.descriptionsDivs.push(description.element);
            }
        }
    };

    presenter.calculateVideoContainerHeight = function ($container, moduleHeight) {
        var borderBottom = $container.css('border-bottom-width'),
            borderTop = $container.css('border-top-width'),
            marginTop = $container.css('margin-top'),
            marginBottom = $container.css('margin-bottom');

        if (ModelValidationUtils.isStringEmpty(borderTop)) borderTop = "0px";
        if (ModelValidationUtils.isStringEmpty(borderBottom)) borderBottom = "0px";
        if (ModelValidationUtils.isStringEmpty(marginTop)) marginTop = "0px";
        if (ModelValidationUtils.isStringEmpty(marginBottom)) marginBottom = "0px";

        return moduleHeight - parseInt(borderBottom, 10) -
            parseInt(borderTop, 10) -
            parseInt(marginTop, 10) -
            parseInt(marginBottom, 10);
    };

    presenter.setDimensions = function () {
        var video = presenter.getVideo();

        presenter.videoContainer.css('height', presenter.calculateVideoContainerHeight(presenter.videoContainer, presenter.configuration.height) + 'px');

        video.css("width", "100%")
            .attr('height', presenter.videoContainer.height());

        presenter.configuration.dimensions = {
            video: {
                width: $(video).width(),
                height: $(video).height()
            },
            container: {
                width: $(presenter.videoContainer).width(),
                height: $(presenter.videoContainer).height()
            }
        };
    };

    presenter.showSubtitles = function () {
        presenter.$view.find('.captions:not(.audio-description)').show();
        presenter.areSubtitlesHidden = false;
    };

    presenter.hideSubtitles = function () {
        presenter.$view.find('.captions:not(.audio-description)').hide();
        presenter.areSubtitlesHidden = true;
    };

    presenter.executeCommand = function (name, params) {
        var commands = {
            'show': presenter.show,
            'hide': presenter.hide,
            'next': presenter.next,
            'previous': presenter.previous,
            'jumpTo': presenter.jumpToCommand,
            'jumpToID': presenter.jumpToIDCommand,
            'seek': presenter.seekCommand,
            'play': presenter.play,
            'stop': presenter.stop,
            'pause': presenter.pause,
            'showSubtitles': presenter.showSubtitles,
            'hideSubtitles': presenter.hideSubtitles,
            'showAudioDescription': presenter.showAudioDescription,
            'hideAudioDescription': presenter.hideAudioDescription,
            'setVideoURL': presenter.setVideoURLCommand
        };

        Commands.dispatch(commands, name, params, presenter);
    };

    presenter.setVideoURLCommand = function (params) {
        presenter.setVideoURL(params[0], params[1]);
    };

    presenter._setVideoURL = function (url, index) {
        var key;
        var videoFile;
        var originalFile;
        var mapper = {
            "oggFormat": "Ogg video",
            "mp4Format": "MP4 video",
            "webMFormat": "WebM video",
            "poster": "Poster",
            "subtitles": "Subtitles",
            "id": "ID",
            "altText": "AlternativeText",
            "loop": "Loop video"
        };

        if (index &gt;= presenter.configuration.files.length) {
            return false;
        }

        videoFile = presenter.configuration.files[index];
        originalFile = presenter.configuration.originalFiles[index];

        for (key in mapper) {
            if (mapper.hasOwnProperty(key)) {
                if (url.hasOwnProperty(key) &amp;&amp; (url[key] == null || url[key] == "")) {
                    videoFile[mapper[key]]  = originalFile[mapper[key]];
                } else {
                    videoFile[mapper[key]] = url[key] || videoFile[mapper[key]];
                }
            }
        }

        if (presenter.addedVideoURLS[index] === undefined) {
            presenter.addedVideoURLS[index] = {
                url: url,
                index: index
            };
        } else {
            for (key in mapper) {
                if (url.hasOwnProperty(key)) {
                    presenter.addedVideoURLS[index].url[key] = url[key];
                }
            }
        }

        return true;
    };

    /*
        Set video url and jump to this video.
        index: video index counted from 0
        url: object {
            "oggFormat": "Ogg video",
            "mp4Format": "MP4 video",
            "webMFormat": "WebM video",
            "poster": "Poster",
            "subtitles": "Subtitles",
            "id": "ID",
            "altText": "AlternativeText",
            "loop": "Loop video"
        }
    */
    presenter.setVideoURL = function (url, index) {
        index = (index || 1) - 1;

        if (presenter._setVideoURL(url, index)) {
            presenter.jumpTo(index + 1);
        }
    };

    presenter.setVisibility = function (isVisible) {
        presenter.$view.css("visibility", isVisible ? "visible" : "hidden");

        var $captions = presenter.$view.find('.captions');
        if (!isVisible) {
            $captions.each(function hideVisibility() {
                $(this).css('visibility', 'hidden');
            });
        } else {
            $captions.each(function showVisibility() {
                if ($(this).attr('visibility') === 'visible') {
                    $(this).css('visibility', 'visible');
                }
            });
        }
    };

    presenter.seek = deferredSyncQueue.decorate(function (seconds) {
        presenter.videoObject.currentTime = seconds;
        if (seconds &gt; presenter.videoObject.duration) {
            presenter.posterPlayButton.removeClass('video-poster-pause');
        }
    });

    presenter.seekFromPercent = function (percent) {
        presenter.seek(presenter.videoObject.duration * (percent / 100));
    };

    presenter.seekCommand = function (params) {
        presenter.seek(params[0]);
    };

    presenter.show = function () {
        if (presenter.isCurrentlyVisible) return;
        if (presenter.VIDEO_STATE.PLAYING == presenter.videoState) {
            presenter.videoObject.play();
        }
        presenter.isCurrentlyVisible = true;
        presenter.setVisibility(true);
    };

    presenter.hide = function () {
        if (!presenter.isCurrentlyVisible) return;

        if (presenter.VIDEO_STATE.PLAYING == presenter.videoState) {
            presenter.videoObject.pause();
            presenter.videoState = presenter.VIDEO_STATE.PLAYING;
            presenter.isHideExecuted = true;
        }
        presenter.isCurrentlyVisible = false;
        presenter.setVisibility(false);
    };

    presenter.jumpTo = function (movieNumber) {
        var newMovie = parseInt(movieNumber, 10) - 1;
        if (0 &lt;= newMovie &amp;&amp; newMovie &lt; presenter.configuration.files.length) {
            presenter.currentMovie = newMovie;
            presenter.reload();
        }
    };

    presenter.jumpToCommand = function (params) {
        presenter.jumpTo(params[0]);
    };

    presenter.jumpToID = function (id) {
        for (var i = 0; i &lt; presenter.configuration.files.length; i++) {
            if (id === presenter.configuration.files[i].ID) {
                presenter.jumpTo(i + 1);  // Video numbers are counted from 1 to n
                break;
            }
        }
    };

    presenter.jumpToIDCommand = function (params) {
        presenter.jumpToID(params[0]);
    };

    presenter.onStalledEventHandler = function () {
        var video = this;

        if (video.readyState &gt;= 2) {
            presenter.isVideoLoaded = true;
            presenter.callTasksFromDeferredQueue();
        }
    };

    presenter.callTasksFromDeferredQueue = function () {
        deferredSyncQueue.resolve();
    };

    presenter.removeWaterMark = function () {
        presenter.$view.find('.poster-wrapper').hide();
    };

    presenter.showWaterMark = function () {
        presenter.$view.find(".poster-wrapper").show();
    };

    presenter.loadVideoAtPlayOnMobiles = function () {
        if (MobileUtils.isSafariMobile(navigator.userAgent)) {
            if (!presenter.isVideoLoaded) {
                presenter.videoObject.load();
                presenter.metadadaLoaded = false;
            }
        }
        if (!presenter.isVideoLoaded) {
            presenter.videoObject.load();
            presenter.metadadaLoaded = false;
        }
    };

    presenter.addClassToView = function (className) {
        presenter.$view.addClass(className);
    };

    presenter.removeClassFromView = function (className) {
        presenter.$view.removeClass(className);
    };

    presenter.play = deferredSyncQueue.decorate(function () {
        presenter.removeWaterMark();
        presenter.hidePlayButton();
        presenter.loadVideoAtPlayOnMobiles();
        presenter.videoObject = presenter.videoContainer.find('video')[0];
        if (presenter.videoObject.paused) {
            presenter.videoObject.play();
            presenter.addClassToView('playing');
        }
        presenter.usedStop = false;
        presenter.playTriggered = true;
    });

    presenter.stop = deferredSyncQueue.decorate(function () {
            presenter.showPlayButton();
            presenter.seek(0);
            presenter.prevTime = -0.001;
            presenter.videoObject.pause();
            presenter.usedStop = true;
            if(presenter.descriptions.length &gt; 0){
                setAudioDescriptionDisabled();
            }
            presenter.removeClassFromView('playing');
            presenter.posterPlayButton.removeClass('video-poster-pause');
    });

    presenter.pause = deferredSyncQueue.decorate(function () {
        if (!presenter.videoObject.paused) {
            presenter.posterPlayButton.addClass('video-poster-pause');
            presenter.showPlayButton();
            presenter.videoObject.pause();
            presenter.removeClassFromView('playing');
        }
        presenter.usedStop = false;
    });

    presenter.previous = function () {
        if (presenter.currentMovie &gt; 0) {
            presenter.currentMovie--;
            presenter.reload();
        }
    };

    presenter.next = function () {
        if (presenter.currentMovie &lt; presenter.configuration.files.length - 1) {
            presenter.currentMovie++;
            presenter.reload();
        }
    };

    presenter.setVolume = function (percent) {
        presenter.videoObject.volume = percent / 100;
    };

    presenter.reset = function () {
        presenter.configuration.isVisibleByDefault ? presenter.show() : presenter.hide();
        presenter.videoState = presenter.VIDEO_STATE.STOPPED;
        presenter.videoObject.currentTime = 0;
        presenter.currentMovie = 0;
        if (presenter.metadadaLoaded) {
            presenter.videoObject.pause();
        }
        presenter.configuration.files = JSON.parse(JSON.stringify(presenter.configuration.originalFiles));
        presenter.reload();

        if (presenter.configuration.shouldHideSubtitles) {
            presenter.hideSubtitles();
        } else {
            presenter.showSubtitles();
        }
        presenter.addedVideoURLS = {};
    };

    presenter.setPlaybackRate = function (playbackRate) {
        presenter.videoObject.playbackRate = parseFloat(playbackRate);
    }

    presenter.updateVideoSpeedController = function() {
        let $playebackRateSelect = presenter.$view.find('.video-playback-rate select');
        if ($playebackRateSelect.length == 0) return;
        $playebackRateSelect.val(presenter.videoObject.playbackRate);
    }

    presenter.getVideo = function () {
        return presenter.videoContainer.find('video:first');
    };

    function generateTransformDict(scaleX, scaleY) {
        var scale = "scale(" + scaleX + "," + scaleY + ")";
        return {
            'transform': scale,
            '-ms-transform': scale,
            '-webkit-transform': scale,
            '-o-transform': scale,
            '-moz-transform': scale,
            "-webkit-transform-origin": "top left",
            "-ms-transform-origin": "top left",
            "transform-origin": "top left"
        }
    }

    function requestFullscreen($element) {
        var DomElement = $element.get(0);

        var requestMethod = DomElement.requestFullscreen || DomElement.mozRequestFullScreen ||
            DomElement.msRequestFullscreen || DomElement.webkitRequestFullScreen ||
            DomElement.webkitEnterFullscreen || null;
        if (requestMethod) {
            requestMethod.call(DomElement);
        }
        return requestMethod;
    }

    function exitFullscreen() {
        var exitMethod = document.exitFullscreen || document.mozCancelFullScreen ||
            document.msExitFullscreen || document.webkitExitFullscreen || null;

        if (exitMethod) {
            exitMethod.call(document);
        }
    }

    function isVideoInFullscreen() {
        if (document.fullscreenElement
            || document.mozFullScreenElement
            || document.webkitFullscreenElement
            || document.msFullscreenElement
            || document.webkitCurrentFullScreenElement
            || document.fullscreen
            || document.webkitIsFullScreen
            || document.mozFullScreen) {
            return true;
        }

        return false;
    }

    presenter.addTabindex = function (isTabindexEnabled) {
        var value = isTabindexEnabled ? "0" : "-1";
        presenter.videoContainer.attr("tabindex", value);
    };

    presenter.getTextToSpeechOrNull = function (playerController) {
        if (playerController) {
            return playerController.getModule('Text_To_Speech1');
        }

        return null;
    };

    function speak (data) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts &amp;&amp; presenter.playerController.isWCAGOn()) {
            tts.speak(data);
        }
    }

    function speakWithCallback (data, callbackFunction) {
        var tts = presenter.getTextToSpeechOrNull(presenter.playerController);
        if (tts) {
            tts.speakWithCallback(data, callbackFunction);
        }
    }

    presenter.isWCAGOn = function(isWCAGOn) {
        //This method has been added to enable the addon's detection by the autofill option of TTS
    };

    return presenter;
}

Addonvideo_create.__supported_player_options__ = {
    interfaceVersion: 2
};
</presenter></addon></addons>